Skip to content

Package scanners: cut false positives and make the CI gate blocking#6355

Merged
danielhanchen merged 3 commits into
mainfrom
scan-packages-fp-hardening
Jun 16, 2026
Merged

Package scanners: cut false positives and make the CI gate blocking#6355
danielhanchen merged 3 commits into
mainfrom
scan-packages-fp-hardening

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Summary

The Python and npm package scanners (scripts/scan_packages.py, scripts/scan_npm_packages.py) red-failed on legitimate library code, so their security-audit.yml steps were left advisory. This reduces the false positives at the source, adds a reviewed baseline allowlist for the residual genuine-library findings, and flips both gates to blocking.

scan_packages.py

  • Scan code only. Blank comments and bare docstrings/doctests before pattern matching, preserving line numbers. Prose, usage examples and >>> doctests no longer trip a finding. This was the dominant false-positive source: pandas doctests, httpx usage examples, advisories quoted in comments.
  • Tighten the anti-analysis regex. Drop the platform.system() ... Linux/Windows/Darwin branch: under re.DOTALL it matched across the whole file, so every cross-platform library tripped it. Also fix the dead /proc/self/status alternative (a leading \b next to / is unsatisfiable).
  • Baseline allowlist (scripts/scan_packages_baseline.json). Reviewed known-good CRITICAL/HIGH findings, matched on (package, basename, check). Only non-baselined CRITICAL/HIGH cause exit 1, and a new kind of finding in a listed file is a different check and still fails. New flags: --baseline, --no-baseline, --write-baseline.
  • sdist coverage. --only-binary :all: never builds an sdist, but a wheel-less project then cannot be fetched at all, and one such package failed the entire --with-deps resolve (exit 2). When the bulk resolve fails, the scanner now drops to per-spec resolution and fetches the raw sdist tarball directly from the PyPI JSON API (no pip build, no setup.py) for static scanning, so every package is covered and no shard exits 2. Genuine transport failures still exit 2.

scan_npm_packages.py

  • Mirror the code-only scanning for JS/TS: blank // and /* */ comments while tracking string, template-literal and regex context so a // inside "http://..." is never treated as a comment. Assigned string literals are never blanked, since droppers hide payloads there.
  • Mirror the baseline allowlist. The npm corpus is clean today, so the committed baseline is empty.

security-audit.yml

  • Flip both scan steps to blocking (SCAN_ENFORCE=1), capturing the scanner exit via PIPESTATUS so tee does not mask it.

Tests

tests/security/test_scan_packages.py and tests/security/test_scan_npm_packages.py gain coverage for the strip, baseline and sdist paths. 52 passing.

Baseline review

The 165 entries in scan_packages_baseline.json are all attested benign across 52 trusted dependencies: Hugging Face auth headers, retry loops, matplotlib's Sphinx exec, langid loading its own model via marshal/eval, vendored test fixtures, and similar. The npm baseline is empty.

danielhanchen and others added 2 commits June 16, 2026 06:58
scan_packages.py and scan_npm_packages.py red-failed on legitimate
library code, so the security-audit steps were left advisory. Reduce
the false positives at the source and flip both gates to blocking.

scan_packages.py:
- Scan code only: blank comments and bare docstrings/doctests before
  matching (line numbers preserved), so prose and >>> examples cannot
  trip a finding.
- Drop the platform.system() branch from the anti-analysis regex (under
  DOTALL it matched across the whole file, so every cross-platform
  library tripped it) and fix the dead /proc/self/status alternative.
- Add a reviewed baseline allowlist (scan_packages_baseline.json) keyed
  on (package, basename, check): only non-baselined CRITICAL/HIGH exit
  1, and a new kind of finding in a listed file still fails.
- sdist fallback: when --with-deps cannot resolve a shard (a sdist-only
  package or a version conflict), drop to per-spec and fetch the raw
  sdist from the PyPI JSON API (no pip build, no setup.py), so every
  package is still scanned and no shard exits 2.

scan_npm_packages.py:
- Mirror the code-only JS/TS scanning (blank // and /* */ comments,
  string/template/regex aware) and the baseline allowlist. The npm
  corpus is clean today, so the baseline is empty.

security-audit.yml:
- Flip both scan steps to blocking (SCAN_ENFORCE=1), capturing the
  scanner exit via PIPESTATUS so tee does not mask it.

tests/security: add coverage for the strip, baseline and sdist paths.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces code-only scanning (comment and docstring stripping) and baseline allowlist support to both the Python and NPM package scanners to significantly reduce false positives. It also implements a secure, direct sdist fallback download mechanism from PyPI to preserve scanning coverage when bulk pip downloads fail, alongside comprehensive unit tests. The reviewer suggested improving error handling in the Python comment stripper by logging tokenization exceptions at a debug level rather than silently catching them.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread scripts/scan_packages.py
Comment on lines +538 to +539

spans: list[tuple[int, int, int, int]] = [] # (srow, scol, erow, ecol)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Avoid using broad, silent exception handlers. When catching Exception to ensure the scanner continues, log the exception (at least at a debug level) to aid in future debugging and troubleshooting.

Suggested change
spans: list[tuple[int, int, int, int]] = [] # (srow, scol, erow, ecol)
except Exception as e:
logger.debug("Failed to tokenize file, falling back to raw content: %s", e)
return content
References
  1. Avoid using broad, silent exception handlers like except Exception: pass. Instead, log the exception, even if at a debug level, to aid in future debugging.

@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: cfadefd267

ℹ️ 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 thread scripts/scan_packages.py
Comment on lines +1721 to +1727
if nd.returncode == 0:
print(
f" [INFO] {name}: full tree unresolvable; scanned the package "
f"alone (--no-deps).",
file = sys.stderr,
)
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.

P1 Badge Do not treat --no-deps fallback as transitive coverage

When a top-level package has a wheel but one of its transitive dependencies is sdist-only, the per-spec pip download --with-deps --only-binary :all: call fails, then this branch downloads only the top-level package and marks the spec successful. I checked pip download --help, and --no-deps means “Don't install package dependencies,” so the sdist-only transitive dependency that caused the failure is never scanned and no download_errors entry is produced; in the now-enforcing CI gate this can report a clean shard while skipping the exact dependency class the fallback was meant to cover.

Useful? React with 👍 / 👎.

Comment thread scripts/scan_packages.py Outdated
continue # optional extra; default install would not pull it
m = _RE_NAME.match(head.strip())
if m:
names.append(m.group(1))

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 Preserve constraints when following sdist dependencies

When a directly fetched sdist declares constrained deps, such as Requires-Dist: payload==1.2.3 or <2, this helper appends only the package name. The follow-up loop later downloads that bare name, so it scans whatever version is current on PyPI rather than the version the sdist would install; a malicious pinned transitive version can be missed while the enforcing CI reports success.

Useful? React with 👍 / 👎.

Comment thread scripts/scan_packages.py
"""
return (_norm_pkg(f.package), os.path.basename(f.filename), f.check)


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 Key baselines by package-relative path

The committed Python baseline is now active in CI, but this key collapses every path in a package to only basename(filename). If a future version of a baselined package adds the same check in another file with a common name such as utils.py or __init__.py, _partition_baseline treats it as already reviewed and exits clean; stripping just the archive root while keeping the package-relative path would preserve version stability without suppressing unrelated files.

Useful? React with 👍 / 👎.

Comment thread scripts/scan_packages.py
while j < n and toks[j].type in (tokenize.COMMENT, tokenize.NL):
j += 1
if j < n and toks[j].type == tokenize.NEWLINE:
spans.append((*tok.start, *tok.end))

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 Preserve executable bare-string payloads

This blanks every bare STRING token before pattern matching. In supported Python 3.10/3.11, a bare f-string such as f"{__import__('os').system('id')}" is tokenized as STRING and executes at import time, and ordinary docstrings can also be executed later with exec(__doc__); after this blanking the scanner sees only exec(__doc__) and can return clean even though the removed string held the network/exec payload. Please only strip truly inert docstrings, or rescan original strings when they are referenced/executable, so malicious packages cannot hide payloads in bare strings.

Useful? React with 👍 / 👎.

- Do not blank f-strings during code-only scanning (they evaluate at
  import); and when a file uses exec/eval, rescan the original for
  payload carriers hidden in a docstring/string so exec(__doc__) style
  payloads stay visible.
- sdist fallback: recover transitive deps with their version specifier
  (fetch the pinned version, not latest), and recover deps in the
  --no-deps branch too so a sdist-only transitive dependency is still
  scanned instead of silently skipped.
- Baseline: key by package-relative path, not basename, so a future
  same-named file in another directory is not auto-suppressed.
  Regenerated the baseline accordingly.

@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: dabccce55f

ℹ️ 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 thread scripts/scan_packages.py
Comment on lines +606 to +610
_HIDDEN_PAYLOAD_PATTERNS = (
(RE_LARGE_BLOB, "large base64 blob"),
(RE_EMBEDDED_KEYS, "embedded key material"),
(RE_MAY12_IOC, "Shai-Hulud IOC string"),
(RE_OBFUSCATION, "marshal/compile/obfuscation"),

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 Scan ordinary docstring payloads passed to exec

If a package stores a plain Python payload in a docstring and then runs exec(__doc__), _strip_noncode removes the docstring before the normal network/subprocess checks, and this new hidden-payload path only restores coverage for blobs, keys, IOCs, or obfuscation. Fresh evidence from the current patch is that an exec(__doc__) file whose docstring contains urllib.request.urlopen(...) plus os.system(...) still produces no findings, so an enforcing shard can report clean while the import-time payload is skipped.

Useful? React with 👍 / 👎.

Comment thread scripts/scan_packages.py
Comment on lines +1831 to +1832
else:
print(f" [WARN] could not resolve indirect dep {dep}; skipping", file = sys.stderr)

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 Do not skip wheel follow-ups after failed indirect resolves

When a recovered dependency is itself a wheel package whose dependency tree fails to resolve because one of its own transitive deps is sdist-only, this branch only prints a warning and does not download the wheel with --no-deps, direct-fetch the sdist child, or add a download_errors entry. Fresh evidence is the new follow-up loop: a direct package A can be scanned, but its dependency B and B's sdist-only child C are silently omitted while the enforcing CI shard can still exit 0.

Useful? React with 👍 / 👎.

Comment thread scripts/scan_packages.py
Comment on lines +1617 to +1618
info = meta.get("info", {}) or {}
reqs = info.get("requires_dist") or []

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 Follow dependency metadata for the pinned sdist

For a pinned sdist fallback, _download_sdist_direct fetches the requested version, but dependency recovery reads meta["info"]["requires_dist"] from the project-level JSON document, which describes the latest release rather than the pinned archive. If oldpkg==1.0.0 is sdist-only and pins a malicious payload==1.0.0, while the latest oldpkg has different or no dependencies, this follows the wrong tree and the enforcing scan can exit clean without ever scanning the payload.

Useful? React with 👍 / 👎.

Comment thread scripts/scan_packages.py
Comment on lines +1624 to +1627
if ";" in r:
head, marker = r.split(";", 1)
if "extra" in marker:
continue # optional extra; default install would not pull it

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 Evaluate extra markers before dropping sdist deps

This skips every Requires-Dist marker containing the word extra, but not every such marker is optional for the default install: extra != 'dev' or python_version >= '3.8' or extra == 'dev' can still be true when no extra is requested. A malicious sdist can put its pinned payload behind one of those default-true markers; pip would install it, but the fallback never adds it to sdist_dep_followups, so the enforcing shard can report clean with that dependency unscanned.

Useful? React with 👍 / 👎.

Comment on lines +1471 to +1473
def _finding_key(f: Finding) -> tuple[str, str, str]:
"""Stable allowlist key: normalized package, file basename, pattern."""
return (_norm_pkg_name(f.package), os.path.basename(f.filename), f.pattern)

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 Key npm baselines by package-relative path

Once a non-empty npm baseline is generated, this key collapses every path in a package to only basename(filename). If a reviewed package has a finding in dist/index.js and a later tarball adds the same pattern in src/index.js or another common basename, _partition_baseline suppresses the new file too and the enforcing npm gate can exit clean; keeping the package-relative path would avoid the over-suppression while remaining stable across version bumps.

Useful? React with 👍 / 👎.

Comment thread scripts/scan_packages.py
Comment on lines +1592 to +1596
if version:
files = meta.get("releases", {}).get(version)
if files:
return files
return meta.get("urls", []) or []

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 Refuse missing pinned releases instead of scanning latest

When a spec pins a version that is absent from PyPI metadata or has no files, this helper falls back to meta["urls"], i.e. the latest release. In the sdist fallback path, pkg==bad-or-yanked-version can therefore be treated as successfully scanned after downloading a different latest sdist, so the enforcing scan exits clean even though the requested pinned artifact was never examined; pinned versions should fail closed when their release file list is missing or empty.

Useful? React with 👍 / 👎.

@danielhanchen danielhanchen merged commit 21612c2 into main Jun 16, 2026
20 of 24 checks passed
@danielhanchen danielhanchen deleted the scan-packages-fp-hardening branch June 16, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant