Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ Results can contain source excerpts, vulnerability details, and reproduction
steps. Keep result directories and saved reports outside the repository and
limit access to authorized reviewers.

Saved-history source excerpts use the Git objects selected when the scan began.
They do not fetch missing objects. Working-tree diff scans omit excerpts because
their uncommitted bytes do not have immutable Git objects. An excerpt can also
be omitted when the original source is unavailable, replacement refs were
present when the scan began, or an older scan did not record enough information
to establish its scope. Later replacement-ref changes do not alter saved source
objects. The finding itself remains available.
Exact, unambiguous historical scopes remain readable for older scans.

### SDK configuration and scan options

Pass runtime configuration to the `CodexSecurity` constructor:
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-security",
"version": "0.1.22",
"version": "0.1.36",
"description": "Codex Security workflows for security scans, analysis, and investigation.",
"author": {
"name": "OpenAI"
Expand Down
11 changes: 9 additions & 2 deletions sdk/typescript/_bundled_plugin/scripts/deep_scan_workbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from filesystem_identity import serialize_filesystem_identity
from finalize_scan_contract import _read_scan_local_json
from workbench.handoff import require_current_continuation
from workbench_source_scopes import capture_source_scopes
from workbench_target import (
directory_content_digest,
directory_snapshot_regular_file_count,
Expand Down Expand Up @@ -786,6 +787,11 @@ def begin_deep_scan_for_target(
terminal["id"],
start_disposition="joined",
)
source_scopes = capture_source_scopes(
target,
(revision, target_snapshot_digest, target_device, target_inode),
[scope],
)
config = effective_deep_scan_config(args)
workflow_version = optional_text(args.workflow_version, maximum=256)
if workflow_version is None:
Expand Down Expand Up @@ -833,10 +839,10 @@ def begin_deep_scan_for_target(
"""
INSERT INTO scans (
id, workspace_id, target_id, target_path, target_revision, target_snapshot_digest,
target_device, target_inode, scope, mode, user_context,
target_device, target_inode, source_scopes_json, scope, mode, user_context,
deep_scan_owner_thread_id, scan_dir, model, reasoning_effort, status, phase,
handoff_status, started_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'deep', ?, ?, ?, ?, ?,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'deep', ?, ?, ?, ?, ?,
'running', 'preflight', 'delivered', ?, ?, ?)
""",
(
Expand All @@ -848,6 +854,7 @@ def begin_deep_scan_for_target(
target_snapshot_digest,
target_device,
target_inode,
json.dumps(source_scopes),
scope,
user_context,
thread_id,
Expand Down
52 changes: 47 additions & 5 deletions sdk/typescript/_bundled_plugin/scripts/workbench_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@
from workbench_schema import (
sql_statements as sql_statements,
)
from workbench_source_excerpt import finding_source_excerpt, safe_source_path
from workbench_source_excerpt import finding_source_excerpt
from workbench_source_scopes import capture_source_scopes, safe_source_path
from workbench_target import (
clean_worktree_content_digest,
copy_directory_excluding,
Expand Down Expand Up @@ -440,6 +441,12 @@ def requested_scan_paths(scan: sqlite3.Row) -> list[str]:
return [scan["scope"]]


def public_scan_recipe(scan: sqlite3.Row) -> dict[str, Any]:
recipe = json.loads(scan["recipe_json"], parse_constant=reject_non_finite_json)
recipe.pop("_codexSecurityFileScopes", None)
return recipe


def scan_contract(scan: sqlite3.Row) -> dict[str, Any]:
target = Path(scan["target_path"])
target_contract = {
Expand Down Expand Up @@ -845,6 +852,12 @@ def start_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict
diff_target,
metadata=target_metadata,
)
source_scopes = capture_source_scopes(
target,
target_identity,
[scope],
diff_target_kind=diff_target["kind"] if diff_target is not None else None,
)
target_root = scan_target_root(args.scan_root, target)
target_root.mkdir(parents=True, exist_ok=True)
if manages_transaction:
Expand Down Expand Up @@ -893,6 +906,7 @@ def start_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict
scope=scope,
diff_target=diff_target,
target_identity=target_identity,
source_scopes=source_scopes,
target_root=target_root,
target_summary=target_summary,
scope_file_count=scope_file_count,
Expand Down Expand Up @@ -949,6 +963,12 @@ def _start_prompt_driven_scan(
)
diff_identity = scan_diff_identity(diff_target)
target_identity = scan_target_identity(target, diff_target)
source_scopes = capture_source_scopes(
target,
target_identity,
[scope],
diff_target_kind=diff_target["kind"] if diff_target is not None else None,
)
target_root = scan_target_root(args.scan_root, target)

connection.execute("BEGIN IMMEDIATE")
Expand Down Expand Up @@ -1048,6 +1068,7 @@ def _start_prompt_driven_scan(
scope=scope,
diff_target=diff_target,
target_identity=target_identity,
source_scopes=source_scopes,
target_root=target_root,
target_summary=target_summary,
scope_file_count=scope_file_count,
Expand Down Expand Up @@ -1616,6 +1637,7 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace)
recipe = parse_scan_recipe(recipe_json, repository)
requested_target = recipe["target"]
paths = requested_target["paths"]
recipe.pop("_codexSecurityFileScopes", None)
scope = paths[0] if len(paths) == 1 else "."
diff_target = None
if requested_target["kind"] in {"refs", "working_tree"}:
Expand All @@ -1633,6 +1655,12 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace)
diff_target["contentDigest"] = worktree_content_digest(repository)
mode = "diff" if diff_target is not None else recipe["mode"]
target_identity = scan_target_identity(repository, diff_target)
source_scopes = capture_source_scopes(
repository,
target_identity,
paths or ["."],
diff_target_kind=diff_target["kind"] if diff_target is not None else None,
)
scope_file_count = (
directory_snapshot_regular_file_count(repository)
if not paths
Expand Down Expand Up @@ -1690,6 +1718,7 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace)
scope=scope,
diff_target=diff_target,
target_identity=target_identity,
source_scopes=source_scopes,
target_root=scan_dir.parent,
target_summary=None,
scope_file_count=scope_file_count,
Expand All @@ -1700,7 +1729,9 @@ def register_cli_scan(connection: sqlite3.Connection, args: argparse.Namespace)
connection.execute(
"UPDATE scans SET recipe_json = ?, parent_scan_id = ?, user_context = ? WHERE id = ?",
(
json.dumps(recipe, allow_nan=False, separators=(",", ":"), sort_keys=True),
json.dumps(
recipe, allow_nan=False, separators=(",", ":"), sort_keys=True
),
parent_scan_id,
user_context,
scan_id,
Expand Down Expand Up @@ -1788,7 +1819,7 @@ def get_scan_recipe(connection: sqlite3.Connection, args: argparse.Namespace) ->
raise SystemExit("This scan does not have a saved launch recipe.")
return {
"parentScanId": scan["parent_scan_id"],
"recipe": json.loads(scan["recipe_json"], parse_constant=reject_non_finite_json),
"recipe": public_scan_recipe(scan),
"scanId": scan["id"],
}

Expand Down Expand Up @@ -3171,7 +3202,7 @@ def scan_context(
}
if scan["recipe_json"] is not None:
context["parentScanId"] = scan["parent_scan_id"]
context["recipe"] = json.loads(scan["recipe_json"], parse_constant=reject_non_finite_json)
context["recipe"] = public_scan_recipe(scan)
return context


Expand Down Expand Up @@ -3454,6 +3485,7 @@ def finding_result(
severity = details.get("severity")
severity = severity if isinstance(severity, dict) else {}
locations = []
excerpt_locations = []
try:
target = require_scan_target_identity(scan)
except SystemExit:
Expand All @@ -3468,6 +3500,14 @@ def finding_result(
""",
(occurrence["id"], FINDING_LOCATIONS_LIMIT),
):
excerpt_locations.append(
{
"endLine": row["end_line"],
"path": row["relative_path"],
"role": row["role"],
"startLine": row["start_line"],
}
)
absolute_path = safe_source_path(target, row["relative_path"]) if target else None
location = {
"endLine": row["end_line"],
Expand Down Expand Up @@ -3512,7 +3552,9 @@ def finding_result(
result["knownSince"] = known_since
result["knownScanIds"] = known_scan_ids
result.pop("artifactPaths", None)
source_excerpt = finding_source_excerpt(scan, target, locations)
source_excerpt = finding_source_excerpt(
scan, target, excerpt_locations, requested_scan_paths(scan)
)
if source_excerpt:
result["sourceExcerpt"] = source_excerpt
artifact_paths = finding_artifact_paths(Path(scan["scan_dir"]), details)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from collections.abc import Callable
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

# Some plugin hosts launch Python with safe-path isolation enabled.
sys.path.insert(0, str(Path(__file__).resolve().parent))
Expand Down Expand Up @@ -157,6 +158,7 @@ def insert_running_scan(
scope: str,
diff_target: dict[str, str] | None,
target_identity: tuple[str, str | None, int | str, int | str],
source_scopes: dict[str, Any],
target_root: Path,
target_summary: str | None,
scope_file_count: int,
Expand All @@ -180,11 +182,11 @@ def insert_running_scan(
"""
INSERT INTO scans (
id, workspace_id, target_id, target_path, target_revision, target_snapshot_digest,
target_device, target_inode, scope, mode, user_context,
target_device, target_inode, source_scopes_json, scope, mode, user_context,
deep_scan_owner_thread_id, diff_target_kind, diff_base_revision,
diff_head_revision, diff_content_digest, target_summary, scan_dir, model,
reasoning_effort, status, phase, handoff_status, started_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
'running', 'preflight', ?, ?, ?, ?)
""",
(
Expand All @@ -193,6 +195,9 @@ def insert_running_scan(
workspace["target_id"],
str(target),
*target_identity,
json.dumps(
source_scopes, allow_nan=False, separators=(",", ":"), sort_keys=True
),
scope,
workspace["default_mode"],
user_context,
Expand Down
22 changes: 22 additions & 0 deletions sdk/typescript/_bundled_plugin/scripts/workbench_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,13 @@
WHERE project_id IS NULL;
""",
),
(
34,
"persist authorized source excerpt scopes",
"""
ALTER TABLE scans ADD COLUMN source_scopes_json TEXT;
""",
),
)


Expand Down Expand Up @@ -744,11 +751,15 @@ def apply_migrations(
"max_time_hours",
"REAL NOT NULL DEFAULT 96",
)
elif version == 34:
add_column_if_missing(connection, "scans", "source_scopes_json", "TEXT")
continue
if version == 6:
repair_thread_scoped_workspaces_migration(connection)
elif version == 16:
should_backfill_targets = repair_stable_targets_migration(connection)
elif version == 34:
add_column_if_missing(connection, "scans", "source_scopes_json", "TEXT")
else:
for statement in sql_statements(sql):
connection.execute(statement)
Expand Down Expand Up @@ -883,6 +894,17 @@ def normalize_pre_release_execution_profile_migrations(


def normalize_pre_release_migrations(connection: sqlite3.Connection, timestamp: str) -> None:
source_scope_migration = connection.execute(
"SELECT name FROM schema_migrations WHERE version = 34"
).fetchone()
if (
source_scope_migration is not None
and source_scope_migration["name"] != "persist authorized source excerpt scopes"
):
raise SystemExit(
"The Codex Security database has an unsupported source-scope migration history."
)

completion_warning_migration = connection.execute(
"SELECT name FROM schema_migrations WHERE version = 25"
).fetchone()
Expand Down
Loading
Loading