Skip to content

feat(scan): add vulnerability classes and entry-point inventory - #200

Open
Haiduongcable wants to merge 5 commits into
openai:mainfrom
Haiduongcable:feat/vulnerability-class-reference
Open

feat(scan): add vulnerability classes and entry-point inventory#200
Haiduongcable wants to merge 5 commits into
openai:mainfrom
Haiduongcable:feat/vulnerability-class-reference

Conversation

@Haiduongcable

Copy link
Copy Markdown
Contributor

Summary

Two related detection-surface additions to the bundled plugin. They are separable and I am happy to
split them into one PR per concern on request.

  1. A vulnerability class reference for classes the concise scan procedures never name.
  2. A deterministic remote entry-point inventory, the plugin's first reachability prior.

Impact

The standard repository and scoped-path scan routes away from the discovery checklist
(skills/finding-discovery/SKILL.md) and relies on a single sentence for its entire vulnerability
taxonomy (repository-wide-scan.md): "unsafe command execution, unsafe parsing, XSS,
attacker-controlled network requests, unsafe file access, and missing permission checks."

Ten classes had zero mentions anywhere in skills/ or references/, verified by grep: race
conditions and TOCTOU, CI/CD workflow injection, prototype pollution, ReDoS, mass assignment,
business-logic and authorization gaps, CORS, token validation, infrastructure and container
configuration, and GraphQL.

Separately, nothing in the plugin knew where untrusted input first reaches a target. No script
mentions any web framework, and generate_rank_input.py only validates a model-supplied score,
so a standard scan reads every file with no reachability signal at all.

Before After
6-category taxonomy for the default scan mode plus 10 classes, each naming the control that defeats it
no entry-point concept http_route, graphql, serverless_handler, message_consumer, server_bind, ci_trigger across 9 languages
reviewed source not marked as untrusted marked in the procedure that reads every byte of the target

Changes

references/vulnerability-classes.md (new). Per class: the code shapes that make it visible in
source, and the specific control that defeats it. Suppression requires naming that control — "the
framework probably handles it"
is explicitly not one. Wired into repository-wide-scan.md and into
finding-discovery/SKILL.md before the workflow split, so diff scans reach it too.

scripts/inventory_entry_points.py (new, model-free). Emits sorted, deduplicated JSONL plus a
summary. Resolves the handler symbol where the syntax carries one. Every read is bounded
(MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, MAX_ROWS, line truncation). Wired into the
standard scan procedure to order review by reachability, stated explicitly as a prior and not a
scope filter — every file in in_scope_files.txt is still reviewed.

tests-ts/entry-point-inventory.test.ts (new). 12 cases shelling the real script via
spawnSync(python, ["-I", "-B", ...]), following the existing scan-recovery.test.ts pattern.

plugin-files.json. Registers both new files; check-package.mjs rejects unregistered ones.

Two shared constants deliberately not reused

Both hide real remote surface, and the tests assert the deviation as a regression guard:

  • generate_rank_input.EXCLUDED_DIRS contains .github, so workflow files are invisible to
    worklist-driven scans. Verified: path_is_excluded(Path(".github/workflows/x.yml")) is True. A
    workflow trigger is among the most directly attacker-reachable entry points a repository has.
  • rank_preview.TEXT_CODE_EXTENSIONS omits .tf, so Terraform is invisible.

Verification

Local, in CI order:

  • run types — clean
  • run test722 pass / 5 skip / 0 fail (33 files)
  • run format — clean
  • run build — clean
  • pnpm pack + run check:package — validates 97 bundled plugin files, 181 tarball entries

A/B comparison of the class reference

Measured directly rather than asserted. Two whole-repo scans of the same fixture commit, same model
(gpt-5.6-sol), same effort (xhigh), same concurrency, with the plugin tree differing in exactly
the three changed files and nothing else. The fixture is a 9-file, 385-line service carrying 19
planted instances across the ten newly covered classes, with negative controls alongside them.

baseline with the reference
discovery candidates 30 42
distinct CWEs raised 22 25
planted instances covered 17 of 19 19 of 19

The reference closed both remaining gaps and raised classes the baseline never surfaced:

  • GraphQL introspection (CWE-200) — enabled unconditionally in the fixture and unreported by
    the baseline.
  • Container image defects — the root default, an unpinned base image, and a secret baked into an
    ENV layer, each filed as its own candidate rather than folded into the compose finding.
  • Writable host bind mount (CWE-732).

It also decomposed authorization far more precisely: six per-resolver candidates for GraphQL
field-level authorization where the baseline filed one, each naming the specific resolver
(User.apiKeys, User.invoices, Organization.members, Organization.billingContact,
User.organization, and the top-level organization query) that resolves without a tenant check.
Both arms independently flagged an Origin-header authorization gap that the fixture author had
mislabelled as safe — the scanner was right and the answer key was wrong.

Scope: both runs were terminated by content-safety filtering (#56) after discovery and before
validation, so these are discovery candidates rather than validated findings, and the richer
treatment output carried a 66% higher estimated spend. One run per arm.

Notes

CI. All eight matrix legs pass on a fork PR at 6fa68d1, so the full matrix is already green
without needing a workflow approval here: ubuntu-latest x node 22 / 24 / 24.0.0 / 26 / 26.0.0,
macos-latest x node 22, windows-latest x node 22 (6m28s), and the linux-amd64 container job.
Zero failures. Exact run links are in the follow-up comment.

Known overlap with in-flight work. #88 and #71 both modify
skills/security-scan/references/repository-wide-scan.md, which this branch also touches to wire in
the two new artifacts. The wiring is a 10-line addition and a one-line pointer; I am glad to rebase
onto whichever of those lands first, or to drop the wiring entirely and ship the script and reference
unwired if you would rather keep that file untouched while it is being reworked.

Projection. references/ and skills/ look like a projection of an internal source rather than
the editable original. Both wiring edits are deliberately minimal for that reason, and they would
need mirroring internally to survive a sync. If the reference file itself is better raised as an
issue than carried as a PR, say so and I will move it.

Scope of the A/B evidence. Both scans were cut short by content-safety filtering (#56) after
discovery and before validation, so the comparison above reports discovery candidates rather than
validated findings, and it is one run per arm on a fixture I wrote. The entry-point inventory's
correctness, determinism, and bounds are verified independently of any model spend.

Not changed deliberately. No schema changes, no TypeScript source changes, _sarif_result and
SARIF_LEVELS untouched, and generate_rank_input.py / rank_preview.py left alone even though the
inventory documents two of their constants as blind spots -- changing shared worklist constants is a
separate decision that belongs with you, not bundled into a new-file addition.

The standard repository and scoped-path scan routes away from the discovery
checklist and relies on a single six-category sentence for its entire
vulnerability taxonomy. Concurrency and TOCTOU, workflow injection, prototype
pollution, ReDoS, mass assignment, business-logic and authorization gaps, CORS,
token validation, infrastructure configuration, and GraphQL had no coverage
anywhere in the skills or references.

Add a shared class reference and wire it into both the standard scan procedure
and discovery so diff scans reach it as well. Each entry names the code shapes
that make the class visible in source and the specific control that defeats it,
so an instance can only be suppressed by naming that control. Also mark reviewed
source as untrusted data in the standard scan procedure, which previously read
every byte of the target without saying so.
Nothing in the plugin knows where untrusted input first reaches a target. No
script mentions any web framework, and the ranking worklist only validates a
model-supplied score, so a standard scan reads every file with no reachability
prior at all.

Add a model-free inventory that reports HTTP routes, GraphQL servers,
serverless handlers, message consumers, server binds, and CI workflow triggers
across Python, JavaScript, TypeScript, Go, Java, Kotlin, Ruby, PHP, and C#,
with the handler symbol resolved where the syntax carries one. Wire it into the
standard scan procedure to order review by reachability. Output is sorted and
deduplicated so repeated runs are byte-identical, and every read is bounded.

The inventory deliberately does not reuse two shared worklist constants,
because both hide remote surface: generate_rank_input.EXCLUDED_DIRS contains
.github, so workflow triggers are invisible to worklist-driven scans, and
rank_preview.TEXT_CODE_EXTENSIONS omits .tf.

State it as a prior, not a verdict: the procedure keeps reviewing every file in
scope, since a file with no entry point can still hold a reachable bug through a
helper.
@chatgpt-codex-connector

Copy link
Copy Markdown

Note

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Haiduongcable

Copy link
Copy Markdown
Contributor Author

Full matrix results from the fork PR at 6fa68d1 (Haiduongcable#4), so the matrix can be confirmed without approving a workflow run here.

Leg Result Duration
categorize release notes skipped --
ubuntu-latest / node-26 success --
linux-amd64 success --
macos-latest / node-22 success --
ubuntu-latest / node-24 success --
ubuntu-latest / node-24.0.0 success --
windows-latest / node-22 success --
ubuntu-latest / node-22 success --
ubuntu-latest / node-26.0.0 success --
publish-linux-${{ matrix.architecture }} skipped --
publish-and-verify-multiarchitecture-candidate skipped --
attest-verified-multiarchitecture-candidate skipped --
promote-verified-and-attested-release skipped --
validate-linux-${{ matrix.architecture }} skipped --
authorize-container-publication skipped --

Zero failures across all eight legs. Local gate in CI order, on the same commit:

  • pnpm --dir sdk/typescript run types — clean
  • pnpm --dir sdk/typescript run test — 722 pass / 5 skip / 0 fail across 33 files
  • pnpm --dir sdk/typescript run format — clean
  • pnpm --dir sdk/typescript run build — clean
  • pnpm pack + pnpm run check:package — 97 bundled plugin files, 181 tarball entries

The branch merges cleanly into main at 3bf2621 with no rebase required.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 1, 2026
@mldangelo-oai mldangelo-oai changed the title feat: add vulnerability class reference and deterministic entry-point inventory feat(scan): add vulnerability classes and entry-point inventory Aug 3, 2026
@Haiduongcable
Haiduongcable force-pushed the feat/vulnerability-class-reference branch 2 times, most recently from 4263997 to e9943c4 Compare August 3, 2026 04:36
@Haiduongcable Haiduongcable reopened this Aug 3, 2026
@mldangelo-oai

Copy link
Copy Markdown
Collaborator

@codex review exact head 05c2461

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05c2461cf9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +331 to +332
for kind, framework, pattern in PATTERNS:
match = pattern.search(line)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict route patterns to their source languages

When a Python application uses the common @app.get(...) form, this loop tests the line against every language's patterns and emits three entries—Express, FastAPI, and Flask—for the same route. Because deduplication includes framework, those entries survive and inflate both entryPointCount and the file-density ranking; select patterns based on the file's language or deduplicate cross-language matches.

Useful? React with 👍 / 👎.

Comment on lines +143 to +144
CI_TRIGGER_RE = re.compile(r"^\s{0,8}(?P<sym>pull_request_target|pull_request|issue_comment|issues|workflow_run|workflow_dispatch|repository_dispatch|push|schedule)\s*:")
CI_ON_RE = re.compile(r"^\s{0,4}on\s*:")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize quoted workflow trigger keys

In workflow files generated or formatted with quoted YAML keys, such as "on": followed by "pull_request_target":, these regular expressions match neither the trigger block nor its entries, so the inventory silently reports no CI entry points. Accept valid quoted mapping keys (or parse the workflow YAML) so the CI surface this feature explicitly prioritizes is not omitted.

Useful? React with 👍 / 👎.

Comment on lines +201 to +202
if len(seen) >= MAX_FILES:
return sorted(seen)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report file-cap truncation in the summary

When a repository contains more than MAX_FILES eligible files, this early return drops the remaining files without communicating that to inventory, leaving summary.truncated false. Consumers therefore treat an incomplete entry-point inventory as complete and may fail to prioritize remotely reachable code in the omitted files; propagate the cap condition into the summary.

Useful? React with 👍 / 👎.

def iter_files(repo: Path, scopes: list[Path]) -> list[Path]:
seen: set[Path] = set()
for scope in scopes:
candidates = [scope] if scope.is_file() else sorted(scope.rglob("*"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prune excluded directories during traversal

For a repository containing a very large excluded tree such as node_modules or vendor, sorted(scope.rglob("*")) traverses and materializes every descendant before path_is_excluded and MAX_FILES are consulted. This defeats the stated bounds and can make inventory generation consume excessive memory or stall on exactly the large or hostile repositories it is intended to handle; use a pruning directory walk and apply the cap while traversing.

Useful? React with 👍 / 👎.

Then inventory the remote entry points deterministically:

```text
<python_command> <plugin_dir>/scripts/inventory_entry_points.py --repo <repo_root> --out <discovery_dir>/entry_points.jsonl --summary <discovery_dir>/entry_points.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass the requested scope to the entry-point inventory

During a scoped-path scan, this documented invocation omits the script's --scope option, so it inventories the entire repository even though in_scope_files.txt contains only the requested path. The resulting artifact includes unrelated entry points and can divert the ordering and trace-inward work outside the user's requested scope; pass the same scope used to construct the file list.

Useful? React with 👍 / 👎.

(KIND_HTTP, "koa", re.compile(r"\b(?:router)\s*\.\s*(?:get|post|put|patch|delete)\s*\(\s*[\"'`/]")),
(KIND_HTTP, "nestjs", re.compile(r"@(?:Get|Post|Put|Patch|Delete|All|Controller)\s*\(")),
(KIND_HTTP, "hapi", re.compile(r"\bserver\s*\.\s*route\s*\(")),
(KIND_SERVERLESS, "node", re.compile(r"\b(?:exports\s*\.\s*handler|module\s*\.\s*exports\s*\.\s*handler)\s*=")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect Node serverless handlers exported as ES modules

For JavaScript or TypeScript Lambda projects using standard ES-module exports such as export const handler = ... or export async function handler(...), this pattern emits no serverless entry point because it recognizes only CommonJS exports.handler forms. These handlers are common in modern Node/TypeScript deployments, so extend the pattern to cover ESM exports rather than silently omitting their remotely reachable surface.

Useful? React with 👍 / 👎.

symbol = match.groupdict().get("sym") if "sym" in pattern.groupindex else None
if symbol is None and kind in {KIND_HTTP, KIND_SERVERLESS}:
symbol = symbol_near(lines, index)
rows.append(_row(relative, index + 1, kind, framework, symbol, line))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the row cap while scanning each file

When one source file contains more than MAX_ROWS matching lines, this append continues until the entire file has been processed because the cap is checked only before starting the next file. A generated or hostile file under the 1 MB byte limit can therefore create hundreds of thousands of row dictionaries before the final slice, defeating the memory bound the cap is meant to provide; stop collecting as soon as the cap is reached inside the line/pattern loops.

Useful? React with 👍 / 👎.

Comment on lines +328 to +330
stripped = line.lstrip()
if stripped.startswith(("#", "//", "*")):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore block-commented entry points

In JavaScript, Java, C#, and other block-commented source, a line such as /* app.get("/removed", handler); */ does not start with any of the prefixes checked here and is emitted as a live route. This makes disabled or explanatory code affect the reachability ordering despite the inventory's existing attempt to exclude commented-out routes; track block-comment state or otherwise remove comments before applying the patterns.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants