From 35bdcf1de18c586a1ca67c3272edc515313643d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 09:32:43 +0900 Subject: [PATCH] fix: bind analysis-run visibility with parameterized SQL literals (v0.98.1) List and detail queries no longer format a WHERE fragment. The $1 / $2 / $3 binds are unchanged. Semgrep no longer treats the predicate as user-concatenated SQL. --- .../0.98.1-analysis-run-sql-constants.md | 4 + CHANGELOG.md | 9 ++ backend/app/analysis_run_ingestion.py | 108 +++++++++++++----- frontend/package.json | 2 +- lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- tests/test_analysis_run_authorization.py | 11 ++ uv.lock | 2 +- 8 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 CHANGELOG.d/0.98.1-analysis-run-sql-constants.md diff --git a/CHANGELOG.d/0.98.1-analysis-run-sql-constants.md b/CHANGELOG.d/0.98.1-analysis-run-sql-constants.md new file mode 100644 index 000000000..37e5d4252 --- /dev/null +++ b/CHANGELOG.d/0.98.1-analysis-run-sql-constants.md @@ -0,0 +1,4 @@ +# 0.98.1 Analysis-run SQL constants + +List and detail visibility queries are complete parameterized literals. +The $1 / $2 / $3 binds are unchanged. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7a2a579..8cec99dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.98.1] - 2026-08-17 + +### Fixed + +- Authorized analysis-run list and detail queries are now complete + parameterized SQL literals. Semgrep no longer treats the visibility + predicate as string-concatenated user input. The $1 / $2 / $3 binds + are unchanged. No TEPP theta is invented. + ## [0.98.0] - 2026-08-17 ### Added diff --git a/backend/app/analysis_run_ingestion.py b/backend/app/analysis_run_ingestion.py index b848c6ca6..8532ee6e1 100644 --- a/backend/app/analysis_run_ingestion.py +++ b/backend/app/analysis_run_ingestion.py @@ -35,35 +35,58 @@ "analysis_run_tepp": "tepp-run-v1", } -_VISIBLE_RUN_SQL = """ - run.requested_by_account_id = $1 - or ( - scope.scope_kind_code = 'analysis_scope_corporate_entity' - and scope.corporate_entity_id = any($2::uuid[]) - ) - or ( - scope.scope_kind_code = 'analysis_scope_process_unit' - and exists ( - select 1 from account_affiliation aff - where aff.user_account_id = $1 - and aff.process_unit_id = scope.process_unit_id +_RUN_LIST_SQL = """ + select + run.analysis_run_id, + run.run_kind_code, + run.knowledge_cutoff, + run.requested_at, + run.configuration_schema_version, + run.configuration_sha256, + run.code_revision_sha, + scope.scope_kind_code, + scope.corporate_entity_id, + scope.process_unit_id, + scope.scope_key, + corp.entity_name as scope_entity_name, + status.status_code, + status.failure_code + from analysis_run run + join analysis_run_scope scope on scope.analysis_run_id = run.analysis_run_id + left join analysis_run_current_status status + on status.analysis_run_id = run.analysis_run_id + left join corporate_entity corp + on corp.corporate_entity_id = scope.corporate_entity_id + where + run.requested_by_account_id = $1 + or ( + scope.scope_kind_code = 'analysis_scope_corporate_entity' + and scope.corporate_entity_id = any($2::uuid[]) ) - ) - or ( - scope.scope_kind_code = 'analysis_scope_thread_group' - and exists ( - select 1 from source_post p - where p.thread_group_key = scope.scope_key - and p.created_at <= run.knowledge_cutoff - and ( - p.visibility_code = 'public' - or p.corporate_entity_id = any($2::uuid[]) - ) + or ( + scope.scope_kind_code = 'analysis_scope_process_unit' + and exists ( + select 1 from account_affiliation aff + where aff.user_account_id = $1 + and aff.process_unit_id = scope.process_unit_id + ) ) - ) + or ( + scope.scope_kind_code = 'analysis_scope_thread_group' + and exists ( + select 1 from source_post p + where p.thread_group_key = scope.scope_key + and p.created_at <= run.knowledge_cutoff + and ( + p.visibility_code = 'public' + or p.corporate_entity_id = any($2::uuid[]) + ) + ) + ) + order by run.requested_at desc """ -_RUN_SELECT = f""" +_RUN_DETAIL_SQL = """ select run.analysis_run_id, run.run_kind_code, @@ -85,7 +108,34 @@ on status.analysis_run_id = run.analysis_run_id left join corporate_entity corp on corp.corporate_entity_id = scope.corporate_entity_id - where {{where}} + where run.analysis_run_id = $3 + and ( + run.requested_by_account_id = $1 + or ( + scope.scope_kind_code = 'analysis_scope_corporate_entity' + and scope.corporate_entity_id = any($2::uuid[]) + ) + or ( + scope.scope_kind_code = 'analysis_scope_process_unit' + and exists ( + select 1 from account_affiliation aff + where aff.user_account_id = $1 + and aff.process_unit_id = scope.process_unit_id + ) + ) + or ( + scope.scope_kind_code = 'analysis_scope_thread_group' + and exists ( + select 1 from source_post p + where p.thread_group_key = scope.scope_key + and p.created_at <= run.knowledge_cutoff + and ( + p.visibility_code = 'public' + or p.corporate_entity_id = any($2::uuid[]) + ) + ) + ) + ) order by run.requested_at desc """ @@ -285,7 +335,7 @@ async def fetch_visible_analysis_runs( ) -> list[dict[str, Any]]: """Runs the account requested or whose scope they may already walk.""" rows = await conn.fetch( - _RUN_SELECT.format(where=_VISIBLE_RUN_SQL), + _RUN_LIST_SQL, account_id, affiliated_entity_ids, ) @@ -300,9 +350,7 @@ async def fetch_visible_analysis_run( ) -> dict[str, Any] | None: """One visible run, or None when it is missing or hidden.""" rows = await conn.fetch( - _RUN_SELECT.format( - where=f"run.analysis_run_id = $3 and ({_VISIBLE_RUN_SQL})" - ), + _RUN_DETAIL_SQL, account_id, affiliated_entity_ids, analysis_run_id, diff --git a/frontend/package.json b/frontend/package.json index 3e22b4931..9385e34e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.98.0", + "version": "0.98.1", "type": "module", "scripts": { "dev": "vite", diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index f8f1f2c33..51fe0b0e7 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -55,4 +55,4 @@ "sentence_excerpts", ] -__version__ = "0.98.0" +__version__ = "0.98.1" diff --git a/pyproject.toml b/pyproject.toml index 14b33ac89..c4e31c721 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "0.98.0" +version = "0.98.1" description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication." readme = "README.md" license = { text = "MIT" } diff --git a/tests/test_analysis_run_authorization.py b/tests/test_analysis_run_authorization.py index 64a0504f7..91de6374f 100644 --- a/tests/test_analysis_run_authorization.py +++ b/tests/test_analysis_run_authorization.py @@ -11,6 +11,8 @@ import pytest from psycopg2 import sql +from backend.app.analysis_run_ingestion import _RUN_DETAIL_SQL, _RUN_LIST_SQL + _ROOT = Path(__file__).resolve().parents[1] _INITIAL_MIGRATION = _ROOT / "migrations" / "0001_initial_schema.sql" _REGISTRY_MIGRATION = _ROOT / "migrations" / "0018_analysis_run_registry.sql" @@ -20,6 +22,15 @@ ) +def test_visible_run_sql_is_parameterized_literals() -> None: + """List and detail queries bind $1/$2/$3; they do not format user SQL.""" + assert "$1" in _RUN_LIST_SQL + assert "$2" in _RUN_LIST_SQL + assert "$3" in _RUN_DETAIL_SQL + assert "{" not in _RUN_LIST_SQL + assert "{" not in _RUN_DETAIL_SQL + + def _postgres_available() -> bool: """Return whether the configured administrator DSN is reachable.""" try: diff --git a/uv.lock b/uv.lock index 4eb0030d7..313a86ed0 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "0.98.0" +version = "0.98.1" source = { virtual = "." } dependencies = [ { name = "certifi" },