Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
NODE_ENV=production
# IMPORTANT: Replace with a strong random value before deploying.
# Generate one with: python3 -c "import secrets; print(secrets.token_urlsafe(48))"
AUTH_SECRET=superscretchangeitplz
# Generate your own master key by running: python3 -c "import os, base64; key = os.urandom(32); print('base64:' + base64.b64encode(key).decode('utf-8'))"
MASTER_VAULT_KEY_V1=base64:qnHTmwYb+uoygIw9MsRMY22vS5YPchY+QOi/E79GAvM=
Expand Down
6 changes: 4 additions & 2 deletions flowsint-api/app/api/routes/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def get_flows(


@router.get("/raw_materials")
async def get_material_list():
async def get_material_list(current_user: Profile = Depends(get_current_user)):
enrichers = ENRICHER_REGISTRY.list_by_categories()
enricher_categories = {
category: [
Expand Down Expand Up @@ -131,7 +131,9 @@ async def get_material_list():


@router.get("/input_type/{input_type}")
async def get_material_by_input_type(input_type: str):
async def get_material_by_input_type(
input_type: str, current_user: Profile = Depends(get_current_user)
):
enrichers = ENRICHER_REGISTRY.list_by_input_type(input_type)
return {"items": enrichers}

Expand Down
8 changes: 5 additions & 3 deletions flowsint-api/app/api/schemas/profile.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from .base import ORMBase
from pydantic import UUID4, BaseModel, ConfigDict, EmailStr
from typing import Optional

from pydantic import UUID4, BaseModel, ConfigDict, EmailStr, Field

from .base import ORMBase


class ProfileCreate(BaseModel):
email: EmailStr
password: str
password: str = Field(..., min_length=8, max_length=128)


class ProfileRead(ORMBase):
Expand Down
42 changes: 25 additions & 17 deletions flowsint-api/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
from fastapi.middleware.cors import CORSMiddleware

# Routes to be included
from app.api.routes import auth
from app.api.routes import investigations
from app.api.routes import sketches
from app.api.routes import enrichers
from app.api.routes import flows
from app.api.routes import events
from app.api.routes import analysis
from app.api.routes import chat
from app.api.routes import scan
from app.api.routes import keys
from app.api.routes import types
from app.api.routes import custom_types
from app.api.routes import enricher_templates
from app.api.routes import (
analysis,
auth,
chat,
custom_types,
enricher_templates,
enrichers,
events,
flows,
investigations,
keys,
scan,
sketches,
types,
)

# Comma-separated list of allowed origins, e.g. "https://app.example.com,https://staging.example.com"
# Falls back to localhost dev origin when unset. Never use "*" with allow_credentials=True.
Expand All @@ -33,8 +35,8 @@
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "Accept"],
)


Expand All @@ -57,5 +59,11 @@ async def health():
app.include_router(scan.router, prefix="/api/scans", tags=["scans"])
app.include_router(keys.router, prefix="/api/keys", tags=["keys"])
app.include_router(types.router, prefix="/api/types", tags=["types"])
app.include_router(custom_types.router, prefix="/api/custom-types", tags=["custom-types"])
app.include_router(enricher_templates.router, prefix="/api/enrichers/templates", tags=["enricher-templates"])
app.include_router(
custom_types.router, prefix="/api/custom-types", tags=["custom-types"]
)
app.include_router(
enricher_templates.router,
prefix="/api/enrichers/templates",
tags=["enricher-templates"],
)
2 changes: 1 addition & 1 deletion flowsint-core/src/flowsint_core/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"AUTH_SECRET environment variable is not set. Please set it in your .env file."
)
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 60
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

Expand Down
43 changes: 36 additions & 7 deletions flowsint-core/src/flowsint_core/core/graph/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,30 @@
handling raw GraphDict object and operations with batching support.
"""

import re
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple

from .connection import Neo4jConnection
from .types import GraphDict

_SAFE_LABEL_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


def _sanitize_label(value: str) -> str:
"""Validate that a Neo4j label/relationship type is safe for interpolation.

Only alphanumeric characters and underscores are allowed (must start with a
letter or underscore). This prevents Cypher injection via crafted
node types or relationship labels.
"""
if not value or not _SAFE_LABEL_RE.match(value):
raise ValueError(
f"Invalid Neo4j label: {value!r}. "
"Labels must contain only alphanumeric characters and underscores."
)
return value


class Neo4jGraphRepository:
"""
Expand Down Expand Up @@ -116,8 +134,10 @@ def _build_node_query(
"created_at": datetime.now(timezone.utc).isoformat(),
}

safe_node_type = _sanitize_label(node_type)

query = f"""
MERGE (n:{node_type} {{ nodeLabel: $node_label, sketch_id: $sketch_id }})
MERGE (n:{safe_node_type} {{ nodeLabel: $node_label, sketch_id: $sketch_id }})
ON CREATE SET n.created_at = $created_at
SET n += $props
SET n.deleted_at = null
Expand All @@ -144,12 +164,16 @@ def _build_relationship_query(
"props": rel_obj,
}

safe_from_type = _sanitize_label(from_type)
safe_to_type = _sanitize_label(to_type)
safe_rel_label = _sanitize_label(rel_label)

query = f"""
MATCH (from:{from_type} {{nodeLabel: $from_label, sketch_id: $sketch_id}})
MATCH (from:{safe_from_type} {{nodeLabel: $from_label, sketch_id: $sketch_id}})
WHERE from.deleted_at IS NULL
MATCH (to:{to_type} {{nodeLabel: $to_label, sketch_id: $sketch_id}})
MATCH (to:{safe_to_type} {{nodeLabel: $to_label, sketch_id: $sketch_id}})
WHERE to.deleted_at IS NULL
MERGE (from)-[r:{rel_label} {{sketch_id: $sketch_id}}]->(to)
MERGE (from)-[r:{safe_rel_label} {{sketch_id: $sketch_id}}]->(to)
SET r += $props
SET r.deleted_at = null
"""
Expand Down Expand Up @@ -356,10 +380,12 @@ def batch_create_edges_by_element_id(
f"{{{props_str}}}" if props_str else "{sketch_id: $sketch_id}"
)

safe_rel_label = _sanitize_label(rel_label)

query = f"""
MATCH (from) WHERE elementId(from) = $from_id_{idx}
MATCH (to) WHERE elementId(to) = $to_id_{idx}
MERGE (from)-[r:`{rel_label}` {rel_props}]->(to)
MERGE (from)-[r:`{safe_rel_label}` {rel_props}]->(to)
"""

# Build params with unique keys for batch execution
Expand Down Expand Up @@ -631,10 +657,12 @@ def create_relationship_by_element_id(
props_str = ", ".join([f"{k}: ${k}" for k in serialized_props.keys()])
rel_props = f"{{{props_str}}}"

safe_rel_label = _sanitize_label(rel_label)

query = f"""
MATCH (a) WHERE elementId(a) = $from_id AND a.deleted_at IS NULL
MATCH (b) WHERE elementId(b) = $to_id AND b.deleted_at IS NULL
MERGE (a)-[r:`{rel_label}` {rel_props}]->(b)
MERGE (a)-[r:`{safe_rel_label}` {rel_props}]->(b)
SET r.deleted_at = null
RETURN properties(r) as rel
"""
Expand Down Expand Up @@ -763,8 +791,9 @@ def merge_nodes(
params = {"nodeId": new_node_id, "sketch_id": sketch_id, **properties}
else:
properties["created_at"] = datetime.now(timezone.utc).isoformat()
safe_node_type = _sanitize_label(node_type)
create_query = f"""
CREATE (n:`{node_type}`)
CREATE (n:`{safe_node_type}`)
SET n = $properties
RETURN elementId(n) as newElementId
"""
Expand Down