← Files Codex SecurityARCHIVED FILE
scripts/generate_rank_input.py
17.5 KB · Oct 7, 2026 · 06:02 UTC
#!/usr/bin/env python3
"""Generate and post-process Codex Security scan worklists.
This script stays deliberately model-free:
- `make-repo-rank-input` creates the deterministic repository or scoped-path
JSONL candidate worklist that ranking subagents consume.
- `make-diff-rank-input` creates the deterministic diff-scoped JSONL candidate
worklist from Git changed paths. It supports committed revision diffs and
local working-tree patches.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
# Some plugin hosts launch Python with safe-path isolation enabled.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_in_scope_files import windows_stream_component
from rank_preview import (
DEFAULT_PREVIEW_BYTES,
DEFAULT_PREVIEW_READ_BYTES,
TEXT_CODE_EXTENSIONS,
is_binary_sample,
preview_for,
preview_for_bytes,
)
from workbench_target import git_blob_bytes, git_command, git_directory_snapshot_paths
EXCLUDED_DIRS = {
".cache",
".circleci",
".devcontainer",
".git",
".github",
".idea",
".mypy_cache",
".pytest_cache",
".ruff_cache",
".tox",
".venv",
".vscode",
"__pycache__",
"bench",
"benchmark",
"bintest",
"build",
"build_config",
"build_configs",
"build-tools",
"build_tools",
"ci",
"coverage",
"deps",
"dev",
"dist",
"doc",
"docs",
"example",
"examples",
"external",
"extern",
"fixture",
"fixtures",
"generated",
"node_modules",
"sample",
"samples",
"target",
"test",
"tests",
"testing",
"third-party",
"third_party",
"tmp",
"vendor",
}
EXCLUDED_FILENAMES = {
".DS_Store",
"CHANGELOG",
"CHANGELOG.md",
"CONTRIBUTING.md",
"Dockerfile",
"Gemfile",
"Gemfile.lock",
"LICENSE",
"LICENSE.md",
"Makefile",
"NEWS",
"NEWS.md",
"NOTICE",
"README",
"README.md",
"README.rst",
"Rakefile",
"SECURITY.md",
"TODO",
"TODO.md",
"docker-compose.yml",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
}
JsonRow = dict[str, object]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Codex Security scan worklist helper.")
subparsers = parser.add_subparsers(dest="command", required=True)
make = subparsers.add_parser(
"make-repo-rank-input",
help="Create rank_input.jsonl for subagent-based file ranking.",
)
make.add_argument("--repo", required=True, help="Repository root.")
make.add_argument(
"--scope",
default=".",
help="Path within the repository to scan. Defaults to the repository root.",
)
make.add_argument(
"--scopes-file",
help="JSON array of repository-relative files and directories to scan together.",
)
make.add_argument("--out", required=True, help="Output rank_input.jsonl path.")
make.add_argument("--area", default="", help="Area label. Defaults to scope.")
make.add_argument(
"--preview-bytes",
type=int,
default=DEFAULT_PREVIEW_BYTES,
help=f"Maximum UTF-8 bytes in each preview. Defaults to {DEFAULT_PREVIEW_BYTES}.",
)
scoped = subparsers.add_parser(
"make-repo-scope-input",
help="List every explicitly scoped file without ranking or reading its contents.",
)
scoped.add_argument("--repo", required=True, help="Repository root.")
scoped.add_argument(
"--scopes-file",
required=True,
help="JSON array of repository-relative files and directories to scan together.",
)
scoped.add_argument("--out", required=True, help="Output scoped-source-input.jsonl path.")
diff = subparsers.add_parser(
"make-diff-rank-input",
help="Create rank_input.jsonl from Git changed source-like files.",
)
diff.add_argument("--repo", required=True, help="Repository root.")
diff.add_argument("--base", required=True, help="Git diff base revision.")
diff.add_argument(
"--mode",
choices=("revisions", "local-patch"),
default="revisions",
help="Git diff mode: committed revisions or staged plus unstaged local patch.",
)
diff.add_argument("--head", default="HEAD", help="Git diff head revision.")
diff.add_argument("--out", required=True, help="Output rank_input.jsonl path.")
diff.add_argument("--area", default="diff", help="Area label for ranking rows.")
diff.add_argument(
"--preview-bytes",
type=int,
default=DEFAULT_PREVIEW_BYTES,
help=f"Maximum UTF-8 bytes in each preview. Defaults to {DEFAULT_PREVIEW_BYTES}.",
)
return parser.parse_args()
def path_is_excluded(path: Path) -> bool:
if any(part in EXCLUDED_DIRS for part in path.parts):
return True
if path.name in EXCLUDED_FILENAMES:
return True
return path.name.endswith((".min.js", ".map"))
def path_is_diff_excluded(path: Path) -> bool:
"""Apply repository exclusions while retaining changed workflow files."""
if path.parts[:2] == (".github", "workflows"):
return False
return path_is_excluded(path)
def resolve_scope(
repo: Path,
scope: str,
*,
expand_user: bool = True,
reject_symlinks: bool = False,
) -> Path:
scope_path = Path(scope).expanduser() if expand_user else Path(scope)
stream = windows_stream_component(scope_path)
if stream is not None:
raise SystemExit(f"Scope must not use an NTFS alternate data stream: {stream}")
if not scope_path.is_absolute():
scope_path = repo / scope_path
if reject_symlinks:
repository = repo.resolve()
try:
relative = scope_path.relative_to(repository)
except ValueError as exc:
raise SystemExit(f"Scope must be inside repo: {scope_path}") from exc
ancestor = repository
for part in relative.parts:
if part == "..":
if ancestor == repository:
raise SystemExit(f"Scope must be inside repo: {scope_path}")
ancestor = ancestor.parent
continue
ancestor /= part
try:
metadata = ancestor.stat(follow_symlinks=False)
except OSError as exc:
raise SystemExit(f"Scope path not found: {ancestor}") from exc
if ancestor.is_symlink() or getattr(metadata, "st_reparse_tag", 0) & 0x20000000:
raise SystemExit(f"Requested scope must not contain symbolic links: {ancestor}")
scope_path = scope_path.resolve()
repo_resolved = repo.resolve()
try:
scope_path.relative_to(repo_resolved)
except ValueError as exc:
raise SystemExit(f"Scope must be inside repo: {scope_path}") from exc
if not scope_path.is_dir() and not scope_path.is_file():
raise SystemExit(f"Scope path not found: {scope_path}")
return scope_path
def write_jsonl(output: Path, rows: list[JsonRow]) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=True, separators=(",", ":")))
handle.write("\n")
def load_scopes_file(scopes_file: Path) -> list[str]:
try:
loaded: object = json.loads(scopes_file.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise SystemExit(f"Unable to read scopes file: {scopes_file}") from exc
if (
not isinstance(loaded, list)
or not loaded
or any(not isinstance(scope, str) or not scope for scope in loaded)
):
raise SystemExit(f"Scopes file must contain a non-empty JSON string array: {scopes_file}")
return loaded
def make_repo_rank_input(args: argparse.Namespace) -> None:
repo = Path(args.repo).expanduser().resolve()
if not repo.is_dir():
raise SystemExit(f"Repo path not found: {repo}")
scopes = [args.scope]
explicit_scopes = args.scopes_file is not None
if explicit_scopes:
scopes = load_scopes_file(Path(args.scopes_file).expanduser())
resolved_scopes = [
resolve_scope(repo, scope, expand_user=not explicit_scopes) for scope in scopes
]
directly_requested_files = {
scope_abs for scope_abs in resolved_scopes if explicit_scopes and scope_abs.is_file()
}
rows_by_path: dict[str, JsonRow] = {}
for scope_abs in resolved_scopes:
scope_rel = scope_abs.relative_to(repo)
area = args.area or scope_rel.as_posix()
candidates = (scope_abs,) if scope_abs.is_file() else scope_abs.rglob("*")
for path in candidates:
try:
if path.is_symlink() or not path.is_file():
continue
path.resolve(strict=True).relative_to(repo)
except (OSError, ValueError):
continue
rel = path.relative_to(repo)
directly_requested = path in directly_requested_files
excluded_path = (
path.relative_to(scope_abs if scope_abs.is_dir() else scope_abs.parent)
if explicit_scopes
else rel
)
if not directly_requested and (
path_is_excluded(excluded_path) or path.suffix.lower() not in TEXT_CODE_EXTENSIONS
):
continue
if (
directly_requested
and path.suffix.lower() not in TEXT_CODE_EXTENSIONS
and path.name not in EXCLUDED_FILENAMES
):
preview = ""
else:
preview, is_binary = preview_for(path, args.preview_bytes)
if is_binary and not directly_requested:
continue
rows_by_path.setdefault(
rel.as_posix(),
{"path": rel.as_posix(), "area": area, "preview": preview},
)
rows = sorted(rows_by_path.values(), key=lambda row: str(row["path"]))
output = Path(args.out).expanduser()
write_jsonl(output, rows)
print(f"Wrote {len(rows)} rows to {output}")
def make_repo_scope_input(args: argparse.Namespace) -> None:
repo = Path(args.repo).expanduser().resolve()
if not repo.is_dir():
raise SystemExit(f"Repo path not found: {repo}")
scopes = load_scopes_file(Path(args.scopes_file).expanduser())
rows_by_path: dict[str, JsonRow] = {}
for scope in scopes:
scope_path = resolve_scope(repo, scope, expand_user=False, reject_symlinks=True)
if scope_path.is_file():
candidates = (scope_path,)
else:
git_candidates = git_directory_snapshot_paths(scope_path)
if git_candidates is not None:
candidates = git_candidates
else:
command = [
"rg",
"--files",
"--hidden",
"--no-require-git",
"--null",
# Also exclude descendants when the scope starts inside .git.
"--glob",
"!**/.git",
"--glob",
"!**/.git/**",
"--",
str(scope_path.relative_to(repo)),
]
try:
result = subprocess.run(command, cwd=repo, capture_output=True, check=False)
except OSError as exc:
ignore_names = (".gitignore", ".ignore", ".rgignore")
ancestors = (scope_path, *scope_path.parents)
has_ignore_rules = (
any((ancestor / ".git").exists() for ancestor in (repo, *repo.parents))
or any(
(ancestor / name).is_file()
for ancestor in ancestors
if ancestor == repo or repo in ancestor.parents
for name in ignore_names
)
or any(
path.name in ignore_names
for path in scope_path.rglob("*")
if path.is_file()
)
)
if has_ignore_rules:
raise SystemExit(
"Could not safely enumerate ignored scoped files without Git or ripgrep."
) from exc
candidates = scope_path.rglob("*")
else:
if result.returncode not in (0, 1):
detail = result.stderr.decode("utf-8", errors="replace").strip()
raise SystemExit(f"Could not enumerate scoped repository files: {detail}")
candidates = (
repo / os.fsdecode(path) for path in result.stdout.split(b"\0") if path
)
for path in candidates:
try:
if path.is_symlink() or not path.is_file():
continue
relative = path.resolve(strict=True).relative_to(repo)
except (OSError, ValueError):
continue
if ".git" in relative.parts:
continue
rows_by_path.setdefault(relative.as_posix(), {"path": relative.as_posix()})
rows = sorted(rows_by_path.values(), key=lambda row: str(row["path"]))
output = Path(args.out).expanduser()
write_jsonl(output, rows)
print(f"Wrote {len(rows)} scoped paths to {output}")
def run_git_changed_paths(repo: Path, diff_args: list[str]) -> list[tuple[Path, str]]:
result = git_command(
repo,
"diff",
"--name-status",
"-z",
"--diff-filter=ACMRD",
*diff_args,
text=False,
)
result.check_returncode()
fields = result.stdout.split(b"\0")
if fields and not fields[-1]:
fields.pop()
changed: list[tuple[Path, str]] = []
index = 0
while index < len(fields):
status = chr(fields[index][0])
index += 1
if status in {"C", "R"}:
index += 1
path = repo / os.fsdecode(fields[index])
index += 1
changed.append((path, status))
return changed
def git_changed_paths(repo: Path, base: str, head: str, mode: str) -> list[tuple[Path, str]]:
if mode == "revisions":
return run_git_changed_paths(repo, [f"{base}..{head}"])
if mode == "local-patch":
unstaged = run_git_changed_paths(repo, [base])
staged = run_git_changed_paths(repo, ["--cached", base])
untracked = git_command(
repo,
"ls-files",
"--others",
"--exclude-standard",
"-z",
text=False,
)
untracked.check_returncode()
combined = dict(staged)
combined.update(unstaged)
combined.update(
(repo / os.fsdecode(relative), "A")
for relative in untracked.stdout.split(b"\0")
if relative
)
return sorted(combined.items())
raise SystemExit(f"Unknown diff mode: {mode}")
def make_diff_rank_input(args: argparse.Namespace) -> None:
repo = Path(args.repo).expanduser().resolve()
if not repo.is_dir():
raise SystemExit(f"Repo path not found: {repo}")
changed = [
(path, status)
for path, status in git_changed_paths(repo, args.base, args.head, args.mode)
if not path_is_diff_excluded(path.relative_to(repo))
and path.suffix.lower() in TEXT_CODE_EXTENSIONS
]
revision_paths = [
path.relative_to(repo)
for path, status in changed
if args.mode == "revisions" and status != "D"
]
revision_blobs = dict(
zip(
revision_paths,
git_blob_bytes(
repo,
[f"{args.head}:{path.as_posix()}" for path in revision_paths],
),
)
)
rows: list[JsonRow] = []
for path, status in changed:
rel = path.relative_to(repo)
preview = ""
if status != "D" and args.mode == "revisions":
content = revision_blobs[rel]
if content is None:
raise SystemExit(
f"Unable to read committed diff blob: {args.head}:{rel.as_posix()}"
)
if is_binary_sample(content):
continue
preview, is_binary = preview_for_bytes(
rel, content[:DEFAULT_PREVIEW_READ_BYTES], args.preview_bytes
)
if is_binary:
continue
elif status != "D" and not path.is_symlink() and path.is_file():
try:
path.resolve(strict=True).relative_to(repo)
except (OSError, ValueError):
preview = ""
else:
preview, is_binary = preview_for(path, args.preview_bytes)
if is_binary:
continue
rows.append({"path": rel.as_posix(), "area": args.area, "preview": preview})
rows.sort(key=lambda row: str(row["path"]))
output = Path(args.out).expanduser()
write_jsonl(output, rows)
print(f"Wrote {len(rows)} rows to {output}")
def main() -> None:
args = parse_args()
if args.command == "make-repo-rank-input":
make_repo_rank_input(args)
elif args.command == "make-repo-scope-input":
make_repo_scope_input(args)
elif args.command == "make-diff-rank-input":
make_diff_rank_input(args)
else:
raise SystemExit(f"Unknown command: {args.command}")
if __name__ == "__main__":
main()
SHA-256: e7c0bddbdb7e9236f7431d84b86be4c3fc202daa8d1306d6baa930110635b671