Skip to content
Draft
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.20",
"version": "0.1.21",
"description": "Codex Security workflows for security scans, analysis, and investigation.",
"author": {
"name": "OpenAI"
Expand Down
91 changes: 89 additions & 2 deletions sdk/typescript/_bundled_plugin/scripts/finalize_scan_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
SCHEMA_VERSION = "1.0"
PRODUCER_NAME = "codex-security-plugin"
FINGERPRINT_ALGORITHM = "codex-security/v1"
REPORT_NAME_RE = re.compile(r"report\.md", re.IGNORECASE | re.ASCII)
SARIF_SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"
SEVERITIES = {"critical", "high", "medium", "low", "informational"}
CONFIDENCES = {"high", "medium", "low"}
Expand Down Expand Up @@ -1011,7 +1012,8 @@ def _validate_derived_finding_identities(
raise ContractError(f"{context}.findingId: does not match derived fingerprint identity")
if finding.get("occurrenceId") != occurrence_id:
raise ContractError(f"{context}.occurrenceId: does not match scan occurrence identity")
if finding.get("fingerprints") != fingerprints:
actual_fingerprints = _require_dict(finding, "fingerprints", context)
if any(actual_fingerprints.get(key) != value for key, value in fingerprints.items()):
raise ContractError(f"{context}.fingerprints: does not match derived fingerprint")


Expand Down Expand Up @@ -1904,6 +1906,82 @@ def _read_sealed_scan(
return manifest, findings, coverage, findings_bytes


def write_report_projection(scan_dir: Path, schema_dir: Path | None = None) -> None:
"""Refresh only an unsealed report, preserving authenticated historical reports."""
scan_dir = _require_scan_directory(scan_dir)
manifest, findings, coverage, _ = _read_sealed_scan(
scan_dir, schema_dir, "report projection"
)
artifact_paths = {
_require_safe_relative_path(artifact["path"], "sealed artifact path")
for artifact in manifest["scan"]["artifacts"]
}
if "report.md" in artifact_paths:
return
try:
output_metadata = (scan_dir / "report.md").stat(follow_symlinks=False)
except FileNotFoundError:
output_metadata = None
except OSError as exc:
raise ContractError("report.md: unable to inspect report output") from exc
same_file_artifacts: list[str] = []
if output_metadata is not None:
for artifact_path in artifact_paths:
descriptor = open_scan_local_file_descriptor(
scan_dir, artifact_path, f"sealed artifact {artifact_path}"
)
try:
artifact_metadata = os.fstat(descriptor)
finally:
os.close(descriptor)
if os.path.samestat(output_metadata, artifact_metadata):
same_file_artifacts.append(artifact_path)
if output_metadata is not None and same_file_artifacts:
try:
entries = set(os.listdir(scan_dir))
report_entries = [name for name in entries if REPORT_NAME_RE.fullmatch(name)]
report_entry = None
if "report.md" in entries:
report_entry = "report.md"
elif len(report_entries) == 1:
report_entry = report_entries[0]
if report_entry is not None and report_entry != "report.md":
descriptor = open_scan_local_file_descriptor(
scan_dir, report_entry, "report directory entry"
)
try:
if not os.path.samestat(output_metadata, os.fstat(descriptor)):
report_entry = None
finally:
os.close(descriptor)
if (
report_entry is not None
and len(report_entries) == 1
and any(REPORT_NAME_RE.fullmatch(path) for path in same_file_artifacts)
):
return
root_metadata = scan_dir.stat(follow_symlinks=False)
for artifact_path in same_file_artifacts:
artifact = PurePosixPath(artifact_path)
parent = _require_scan_directory(scan_dir.joinpath(*artifact.parts[:-1]))
if not os.path.samestat(root_metadata, parent.stat(follow_symlinks=False)):
continue
if (
report_entry is not None
and artifact.name in entries
and artifact.name != report_entry
):
continue
raise ContractError(
"report.md: cannot safely replace an ambiguous sealed artifact alias"
)
except OSError as exc:
raise ContractError("report.md: unable to inspect sealed artifact aliases") from exc
write_scan_local_bytes(
scan_dir, "report.md", _generate_report_projection(manifest, findings, coverage)
)


def build_sarif_projection(
scan_dir: Path, source_root: Path | None = None, schema_dir: Path | None = None
) -> dict[str, Any]:
Expand Down Expand Up @@ -2316,18 +2394,27 @@ def main() -> int:
parser.add_argument("--schema-dir", type=Path)
parser.add_argument("--source-root", type=Path)
parser.add_argument("--sarif-only", action="store_true")
parser.add_argument("--report-only", action="store_true")
parser.add_argument("--sarif-output", type=Path)
parser.add_argument("--export-format", choices=sorted(EXPORT_PATHS))
parser.add_argument("--export-output", type=Path)
args = parser.parse_args()
try:
if args.report_only and (
args.sarif_only or args.export_format is not None or args.source_root is not None
):
parser.error(
"--report-only cannot be combined with SARIF, export, or source-root options"
)
if args.sarif_only and args.export_format is not None:
parser.error("--sarif-only cannot be combined with --export-format")
if args.export_output is not None and args.export_format is None:
parser.error("--export-output requires --export-format")
if args.sarif_output is not None and not args.sarif_only:
parser.error("--sarif-output requires --sarif-only")
if args.export_format is not None:
if args.report_only:
write_report_projection(args.scan_dir, args.schema_dir)
elif args.export_format is not None:
contents = build_findings_export(
args.scan_dir, args.export_format, args.source_root, args.schema_dir
)
Expand Down
60 changes: 60 additions & 0 deletions sdk/typescript/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { constants, type Stats } from "node:fs";
import {
lstat,
open,
readdir,
readFile,
realpath,
type FileHandle,
Expand All @@ -27,6 +28,7 @@ const DOCUMENTS = {
"coverage.json": "coverage.schema.json",
} as const;
const PRODUCER_NAME = "codex-security-plugin";
const REPORT_NAME = /^[rR][eE][pP][oO][rR][tT]\.[mM][dD]$/u;
const SAFE_SCHEMA_ERROR_PROPERTIES = new Set([
"scan",
"target",
Expand Down Expand Up @@ -291,6 +293,64 @@ export async function requireScanFile(
).path;
}

// The manifest must already have passed loadContract's seal validation.
export async function hasSealedReport(
scanDirectory: string,
manifest: ScanManifest,
signal?: AbortSignal,
): Promise<boolean> {
try {
throwIfAborted(signal);
const artifactPaths = manifest.scan.artifacts
.map((artifact, index) =>
safeRelativePath(
artifact.path,
`manifest.scan.artifacts[${index}].path`,
),
)
.filter((path) => REPORT_NAME.test(path));
if (artifactPaths.includes("report.md")) return true;
if (artifactPaths.length === 0) return false;

const scanRoot = await requireScanRoot(scanDirectory, signal);
const reportEntries = (await readdir(scanRoot.path)).filter((name) =>
REPORT_NAME.test(name),
);
throwIfAborted(signal);
if (reportEntries.length !== 1) return false;
const report = await requireCheckedScanFile(
scanRoot.path,
"report.md",
"report.md",
signal,
scanRoot,
);
for (const artifactPath of artifactPaths) {
const file = await openCheckedScanFile(
scanRoot.path,
artifactPath,
`sealed artifact ${artifactPath}`,
signal,
scanRoot,
);
try {
const sameFile = await sameCheckedFileDevice(
file,
report,
await file.stat(),
);
throwIfAborted(signal);
if (sameFile) return true;
Comment on lines +337 to +343

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Regenerate hard-linked report aliases

When an unsealed report.md is a hard link to any sealed artifact, such as findings.json, inode identity makes this return true; runCampaign therefore skips report recovery and resumes with JSON or another artifact masquerading as the human-facing report. The Python --report-only path has the same hard-link exemption, even though its atomic os.replace writer can safely replace the alias without modifying the sealed inode. The newly added hard-link acceptance is fresh evidence for the previously raised report-validation issue; only treat an actual sealed report.md artifact as reusable and regenerate all aliases.

AGENTS.md reference: sdk/typescript/AGENTS.md:L19-L20

Useful? React with 👍 / 👎.

} finally {
await file.close();
}
}
} catch {
throwIfAborted(signal);
}
return false;
}

async function requireCheckedScanFile(
scanDirectory: string,
relativePath: string,
Expand Down
Loading
Loading