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
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ container credentials, and the default AWS credential chain.

Local sign-in honors Codex's configured credential backend, including a system
keyring required by a managed device. Codex Security keeps login and scan
credentials in the same private, persistent state directory.
sign-in scoped to a dedicated Codex home in the same private, persistent state
directory.

If both a ChatGPT sign-in and an API key are available, interactive scans ask
which credential to use. CI and other noninteractive scans keep the existing
Expand All @@ -67,12 +68,26 @@ keys:
unset OPENAI_API_KEY CODEX_API_KEY
```

Scan history is stored in the Codex Security workbench state directory. If that
directory cannot be written, set `CODEX_SECURITY_STATE_DIR` to a writable
directory outside the repository.
Scan history and saved findings are stored in one Codex Security state database.
Linked Git worktrees are discovered automatically and grouped when they use the
same state directory. Leave `CODEX_SECURITY_STATE_DIR` unset, or select one
stable, writable directory outside the repository. Changing or unsetting it
selects separate history and an isolated Codex credential home and sign-in
scope; restore the previous value to reopen its existing scans and sign-in.

```bash
# Run from another linked Git worktree:
npx @openai/codex-security scans list
npx @openai/codex-security findings list

# Reopen an existing, separately selected state directory:
export CODEX_SECURITY_STATE_DIR=/path/to/existing/codex-security-state
npx @openai/codex-security scans list
```

`findings list [repository]` shows open findings across a repository's scans
and identifies findings not confirmed in its latest scan.
and identifies findings not confirmed in its latest completed scan across
linked worktrees.

`scans compare BEFORE_SCAN_ID AFTER_SCAN_ID` automatically matches findings by
root cause, reuses saved matches, and identifies new, persisting, reopened,
Expand Down
40 changes: 31 additions & 9 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ The CLI and SDK recognize the following user-configurable environment:
| `OPENAI_API_KEY`, `CODEX_API_KEY` | Scan authentication; `OPENAI_API_KEY` wins when both are present. |
| `CODEX_SECURITY_LOG_LEVEL` | CLI-only; set to `debug` for verbose diagnostics. |
| `LOG_LEVEL` | CLI-only fallback when `CODEX_SECURITY_LOG_LEVEL` is unset. |
| `CODEX_SECURITY_STATE_DIR` | Override the private scan-history, workbench, and default artifact directory. |
| `CODEX_SECURITY_STATE_DIR` | Select the history database, artifact directory, and dedicated Codex credential home. |
| `CODEX_HOME` | Set the ambient Codex home for file-backed sign-in and default state; defaults to `~/.codex`. |
| `CODEX_CLI_PATH` | Use another Codex executable for authentication, plugin setup, scans, and nested workers. |
| `PYTHON` | Select a Python interpreter when `--python` or SDK `pythonPath` is not set. |
Expand Down Expand Up @@ -538,11 +538,15 @@ same command to resume.

### Scan history and reruns

`npx @openai/codex-security scans list` lists scans for the current repository. Pass a
repository path to inspect another checkout, `--scan-root DIR` to list scans
whose artifacts are under a particular root. `scans show SCAN_ID` includes the
scan configuration, results, coverage, and artifact locations. Add
`--show-linked-findings` to include finding links from previous scans.
`npx @openai/codex-security scans list` lists scans for the current repository.
Linked Git worktrees are discovered automatically and grouped when they share
the selected state database. Findings indicate whether they were confirmed in
the repository's latest completed scan across those linked worktrees. Pass a
repository path to inspect another checkout. `--scan-root DIR` only filters
scans already recorded in that database by their artifact directory; it never
imports scan results from another state directory. `scans show SCAN_ID`
includes the scan configuration, results, coverage, and artifact locations.
Add `--show-linked-findings` to include finding links from previous scans.

`scans logs SCAN_ID` shows complete session events from the scan and its
workers, which can include source code and credentials.
Expand All @@ -553,9 +557,27 @@ least eight characters.
Scan history uses `$CODEX_SECURITY_STATE_DIR/workbench.sqlite3` when
`CODEX_SECURITY_STATE_DIR` is set. Otherwise, it uses
`$CODEX_HOME/state/plugins/codex-security/workbench.sqlite3`; `CODEX_HOME`
defaults to `~/.codex`. Scan credentials are never stored in the scan
configuration. Recorded failure summaries and bulk-scan receipts omit messages
that contain recognizable credentials.
defaults to `~/.codex`. Saved findings use the same selected database.
Changing or unsetting `CODEX_SECURITY_STATE_DIR` selects separate scan history
and an isolated Codex credential home and sign-in scope; scans from the
previous state remain hidden until you select that state again. Keep the
setting stable across linked worktrees to share scans and findings
automatically.

```bash
# Inspect shared history from another linked Git worktree:
npx @openai/codex-security scans list
npx @openai/codex-security findings list

# Reopen an existing state directory and its sign-in:
export CODEX_SECURITY_STATE_DIR=/path/to/existing/codex-security-state
npx @openai/codex-security scans list
npx @openai/codex-security findings list
```

Scan credentials are never stored in the scan configuration. Recorded failure
summaries and bulk-scan receipts omit messages that contain recognizable
credentials.

The scan sandbox permits writes to the selected state directory so SQLite can
maintain its database and journal files. If the host itself cannot write to the
Expand Down
32 changes: 28 additions & 4 deletions sdk/typescript/_bundled_plugin/scripts/workbench_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,24 @@
FINDING_SUMMARY_BYTES,
FINDING_TITLE_BYTES,
)
from workbench_native_indexes import _indexed_findings, repository_target_ids
from workbench_validation import bounded_output_text


def get_scan_feedback(connection: sqlite3.Connection, scan: sqlite3.Row) -> dict[str, Any]:
target_ids = sorted(repository_target_ids(connection, scan["target_id"]))
if not target_ids:
return {"scanId": scan["id"], "targetId": scan["target_id"], "falsePositives": []}

indexed_findings = {
finding_id: finding
for finding in _indexed_findings(connection)
if finding["target_id"] in target_ids
for finding_id in finding["matched_finding_ids"]
}
target_placeholders = ", ".join("?" for _ in target_ids)
rows = connection.execute(
"""
f"""
WITH ranked_decisions AS (
SELECT findings.id AS finding_id, findings.fingerprint, findings.rule_id,
findings.identity_anchor, findings.identity_instance, occurrences.title,
Expand Down Expand Up @@ -49,7 +61,7 @@ def get_scan_feedback(connection: sqlite3.Connection, scan: sqlite3.Row) -> dict
candidate.sort_order
LIMIT 1
)
WHERE source_scans.target_id = ?
WHERE source_scans.target_id IN ({target_placeholders})
AND source_scans.id != ?
AND source_scans.status = 'complete'
)
Expand All @@ -61,12 +73,22 @@ def get_scan_feedback(connection: sqlite3.Connection, scan: sqlite3.Row) -> dict
AND note IS NOT NULL
AND trim(note) != ''
ORDER BY updated_at DESC, source_completed_at DESC, source_scan_id DESC, finding_id DESC
LIMIT 50
""",
(scan["target_id"], scan["id"]),
(*target_ids, scan["id"]),
)
false_positives = []
reviewed_components: set[str] = set()
for row in rows:
finding = indexed_findings.get(row["finding_id"])
if (
finding is None
or finding["status"] != "closed"
or finding["close_reason"] != "false_positive"
or finding["occurrence_id"] in reviewed_components
):
continue
reviewed_components.add(finding["occurrence_id"])

identity = {"anchor": row["identity_anchor"]}
if row["identity_instance"] is not None:
identity["instance"] = row["identity_instance"]
Expand All @@ -91,6 +113,8 @@ def get_scan_feedback(connection: sqlite3.Connection, scan: sqlite3.Row) -> dict
"updatedAt": row["updated_at"],
}
)
if len(false_positives) == 50:
break
return {"scanId": scan["id"], "targetId": scan["target_id"], "falsePositives": false_positives}


Expand Down
Loading
Loading