Symptom
Semgrep (multi-language SAST) fails with "Semgrep found WARNING/ERROR (Medium+) findings" on .github PR #2065 (run 35240824316, job 105268505006), against merged head f2b5ecae4865ef2ba630d15abcde007ba57bd126:
SEMGREP_FINDING rule=python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected level=warning path=scripts/ci/codeql_ghas_configuration_identity.py line=158 message=Detected a dynamic value being used with urllib. ...
SEMGREP_FINDING rule=python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected level=warning path=scripts/ci/strix_evidence_binding.py line=264 message=Detected a dynamic value being used with urllib. ...
Update: Bandit (Python SAST) (job 105335869601, same PR, same head) independently failed on the same two call sites with its own rule for the identical pattern:
B310: Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected. (severity=MEDIUM, confidence=HIGH)
(2 findings — Bandit's console output doesn't echo path/line, but scripts/ci/ has exactly two unsuppressed urlopen() call sites on main, both in the files above; see below.)
Both flagged lines are on main, unmodified by PR #2065 (whose own diff is .gitleaks.toml only) — confirmed via git grep -n "urlopen(" origin/main -- scripts/.
Why this is a false positive
scripts/ci/codeql_ghas_configuration_identity.py:158 — _request_json(url, ...) calls urllib.request.urlopen(request, ...) where request wraps url. Its only caller (list_codeql_analyses, line ~191) builds url as f"https://api.github.com/repos/{repository}/code-scanning/analyses?{query}" — the scheme and host are a fixed literal; only the owner/repo path segment and a urlencoded query string are dynamic.
scripts/ci/strix_evidence_binding.py:264 — same shape: Request(url, ...) then urlopen(request, timeout=30) # noqa: S310 - GitHub HTTPS only, with url built against the fixed https://api.github.com base.
Semgrep's dynamic-urllib-use-detected and Bandit's B310 both flag any urlopen() call whose argument isn't a string literal, regardless of whether the dynamic part can actually change the scheme/host. Neither url here is influenced by PR input, issue/comment bodies, or any other untrusted source — both are internal GitHub REST API calls with a hardcoded trusted host. Same character as the CodeQL false positive already tracked and fixed in #2208 (py/incomplete-url-substring-sanitization), just two more scanners/rules catching the same syntactic pattern in the same two files.
Notable finding: strix_evidence_binding.py:264 already carries a # noqa: S310 comment — but # noqa is the Ruff/flake8 suppression syntax, not Bandit's. Bandit only recognizes # nosec or # nosec B310. So this line already shows someone reasoned about the Bandit finding and tried to suppress it, but used the wrong directive — it has never actually silenced Bandit. scripts/ci/materialize_base_python_requirements.py:363 shows the correct dual-suppression pattern already in use elsewhere in this same repo: # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310.
Why it will affect every PR
Like #2208, the findings are on main and unscoped by any Semgrep/Bandit config (sast-semgrep.yml passes only --config=p/default, --exclude=.github/workflows, --exclude='docs/research/**/standards'; no path-ignore for scripts/ci). Every PR that triggers a fresh whole-tree scan will hit both until resolved on main.
Remediation options (mirroring #2208's structure)
- Narrow, no new mechanism: assert the resolved URL's scheme/host immediately before each
urlopen/Request call, e.g. assert url.startswith("https://api.github.com/"), so the dynamic value is validated in a way both scanners' heuristics recognize. Two call sites, two files.
- Suppress correctly: replace
strix_evidence_binding.py:264's # noqa: S310 with the same dual-suppression comment already used at materialize_base_python_requirements.py:363 (# nosemgrep: ... # nosec B310), and add the equivalent to codeql_ghas_configuration_identity.py:158 (currently unsuppressed for either tool). Fixes both scanners at once, matches existing repo convention, but doesn't remove the underlying heuristic mismatch the way option 1 does.
- Scope the scan to exclude
scripts/ci from these rules — broadest effect, needs its own justification since it would stop scanning that directory for this whole rule class, for both scanners.
Either option 1 or 2 looks right (2 is the path of least resistance since materialize_base_python_requirements.py already establishes the convention), but I am not the writer of codeql_ghas_configuration_identity.py or strix_evidence_binding.py and my open PR (#2065) doesn't touch either file, so I'm not taking it there — filing this per the same not-bypass-eligible governance rule #2208 documented.
Notes for whoever picks this up
- Zero open PRs currently touch either flagged file (checked via
search_issues/PR search at filing time), so the lane should be free.
- Whichever option is chosen, fix both call sites together — partial suppression (e.g. only Semgrep or only Bandit) will leave the other scanner red on the next PR.
Filed from PR #2065, whose own diff is one unrelated .gitleaks.toml allowlist entry.
Symptom
Semgrep (multi-language SAST)fails with "Semgrep found WARNING/ERROR (Medium+) findings" on.githubPR #2065 (run35240824316, job105268505006), against merged headf2b5ecae4865ef2ba630d15abcde007ba57bd126:Update:
Bandit (Python SAST)(job105335869601, same PR, same head) independently failed on the same two call sites with its own rule for the identical pattern:(2 findings — Bandit's console output doesn't echo path/line, but
scripts/ci/has exactly two unsuppressedurlopen()call sites onmain, both in the files above; see below.)Both flagged lines are on
main, unmodified by PR #2065 (whose own diff is.gitleaks.tomlonly) — confirmed viagit grep -n "urlopen(" origin/main -- scripts/.Why this is a false positive
scripts/ci/codeql_ghas_configuration_identity.py:158—_request_json(url, ...)callsurllib.request.urlopen(request, ...)whererequestwrapsurl. Its only caller (list_codeql_analyses, line ~191) buildsurlasf"https://api.github.com/repos/{repository}/code-scanning/analyses?{query}"— the scheme and host are a fixed literal; only theowner/repopath segment and aurlencoded query string are dynamic.scripts/ci/strix_evidence_binding.py:264— same shape:Request(url, ...)thenurlopen(request, timeout=30) # noqa: S310 - GitHub HTTPS only, withurlbuilt against the fixedhttps://api.github.combase.Semgrep's
dynamic-urllib-use-detectedand Bandit'sB310both flag anyurlopen()call whose argument isn't a string literal, regardless of whether the dynamic part can actually change the scheme/host. Neitherurlhere is influenced by PR input, issue/comment bodies, or any other untrusted source — both are internal GitHub REST API calls with a hardcoded trusted host. Same character as the CodeQL false positive already tracked and fixed in #2208 (py/incomplete-url-substring-sanitization), just two more scanners/rules catching the same syntactic pattern in the same two files.Notable finding:
strix_evidence_binding.py:264already carries a# noqa: S310comment — but# noqais the Ruff/flake8 suppression syntax, not Bandit's. Bandit only recognizes# nosecor# nosec B310. So this line already shows someone reasoned about the Bandit finding and tried to suppress it, but used the wrong directive — it has never actually silenced Bandit.scripts/ci/materialize_base_python_requirements.py:363shows the correct dual-suppression pattern already in use elsewhere in this same repo:# nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310.Why it will affect every PR
Like #2208, the findings are on
mainand unscoped by any Semgrep/Bandit config (sast-semgrep.ymlpasses only--config=p/default,--exclude=.github/workflows,--exclude='docs/research/**/standards'; no path-ignore forscripts/ci). Every PR that triggers a fresh whole-tree scan will hit both until resolved onmain.Remediation options (mirroring #2208's structure)
urlopen/Requestcall, e.g.assert url.startswith("https://api.github.com/"), so the dynamic value is validated in a way both scanners' heuristics recognize. Two call sites, two files.strix_evidence_binding.py:264's# noqa: S310with the same dual-suppression comment already used atmaterialize_base_python_requirements.py:363(# nosemgrep: ... # nosec B310), and add the equivalent tocodeql_ghas_configuration_identity.py:158(currently unsuppressed for either tool). Fixes both scanners at once, matches existing repo convention, but doesn't remove the underlying heuristic mismatch the way option 1 does.scripts/cifrom these rules — broadest effect, needs its own justification since it would stop scanning that directory for this whole rule class, for both scanners.Either option 1 or 2 looks right (2 is the path of least resistance since
materialize_base_python_requirements.pyalready establishes the convention), but I am not the writer ofcodeql_ghas_configuration_identity.pyorstrix_evidence_binding.pyand my open PR (#2065) doesn't touch either file, so I'm not taking it there — filing this per the same not-bypass-eligible governance rule #2208 documented.Notes for whoever picks this up
search_issues/PR search at filing time), so the lane should be free.Filed from PR #2065, whose own diff is one unrelated
.gitleaks.tomlallowlist entry.