mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
data: hunt training-set candidates (task 4.1), not frozen: 99 verified locations in 28 changes, awaiting spot-check
Sourced apart from the 12 pilot repositories: 212 candidates (pool-fresh, pool-retry, OSV for PyPI, npm, Maven and Packagist), 49 dropped because the benchmarks already name their repository, 413 catalog locations, 99 passing all three verifier checks in 28 repositories. Short of the 40-positive target. catalog.py now reads .java and .php. Freeze waits on the maintainer's spot-check (CANDIDATES.md, SOURCING.md). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
25ae4b489c
commit
2b65680d5d
@@ -0,0 +1,363 @@
|
||||
# Training-set candidates for the maintainer's spot-check
|
||||
|
||||
Generated 2026-10-13 by `benchmarks/valid/train/sourcing.py candidates`. Every location below passed all three
|
||||
model checks (vulnerable before the fix, fix addresses it, reachable from an in-repo entry point) and waits for a
|
||||
human accept or reject. Nothing here is a positive until accepted. Accept or reject a location with
|
||||
|
||||
.venv/bin/python -m benchmarks.valid.verify spot-check benchmarks/valid/train/verification.json <id> accept|reject --reviewer <you> --date YYYY-MM-DD
|
||||
|
||||
Sites passing: 99 in 28 repositories, 28 positive changes (fix commits); languages: java 21, javascript 5, php 31, python 21, typescript 21
|
||||
|
||||
The verifier's entry point is the model's claim, not a checked fact; it is the reason the site passed the reachability check.
|
||||
|
||||
## andrea9293/mcp-documentation-server
|
||||
|
||||
- advisory: GHSA-6f5r-5672-72j7 (access_control, CWE-306), origin `osv`, ecosystem npm, licence MIT
|
||||
- fix `37159d4e06b8ee50c3645b2496e3d3f6d32c47f9`, parent (fix^1) `06d28086dd3fe120978bfa4a49a0a93c7bba8449`
|
||||
- advisory title: @andrea9293/mcp-documentation-server: Web UI API binds to all interfaces without authentication by default
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `andrea9293-mcp-documentation-server-0913a4303cf82c9a` | `src/web-server.ts::startWebServer` (24-41) | typescript | startWebServer -> app.listen(PORT) binding all interfaces; HTTP routes GET /api/documents, POST /api/documents, DELETE /api/documents/:id, POST /api/search-all, GET /api/config | 0.9 |
|
||||
|
||||
## apache/atlas
|
||||
|
||||
- advisory: GHSA-v73p-f52r-fmmr (output_encoding, CWE-80), origin `osv`, ecosystem maven, licence Apache-2.0
|
||||
- fix `800f979307d6ea0f0b6563ec890f6b728022e497`, parent (fix^1) `e67cc199e61afc92f242cccd05e97a61e0791bd2`
|
||||
- advisory title: Apache Atlas UI: Authenticated User XSS
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `apache-atlas-3f751971e2a63d2e` | `dashboardv2/public/js/views/search/SearchResultLayoutView.js::function` (34-1474) | javascript | SearchResultLayoutView.getFixedDslColumn / getDaynamicColumns formatters rendering entity attributes via Backgrid Html cells; source is search API response entity attributes (e.g. name, typeName, cust | 0.55 |
|
||||
| `apache-atlas-42a370a3cdec7d12` | `dashboardv2/public/js/views/site/Header.js::function` (27-347) | javascript | Header view onRender: that.$('.userName').html(Globals.userLogedIn.response.userName) renders the authenticated user's userName via .html() (DOM XSS sink); source is the logged-in user's userName from | 0.75 |
|
||||
| `apache-atlas-4c582bc549c555a1` | `dashboard/src/components/FilterQuery.tsx::clearQueryAttr` (97-503) | typescript | React route /search/searchResult (FilterQuery component); source: URL query string via location.search / URLSearchParams, attacker-controlled filter params (type, tag, relationshipName, entityFilters, | 0.55 |
|
||||
| `apache-atlas-940ab56a7aadcda1` | `dashboardv2/public/js/views/search/RelationSearchResultLayoutView.js::function` (33-820) | javascript | Backbone Marionette view RelationSearchResultLayoutView rendering relationship search results; data from relationship search API (getBasicRearchResult/fetch) flows into Backgrid Html cell formatters ( | 0.55 |
|
||||
|
||||
## apache/pdfbox
|
||||
|
||||
- advisory: GHSA-jjwr-xmw6-gf78 (path, CWE-22), origin `osv-extend`, ecosystem maven, licence Apache-2.0
|
||||
- fix `b028eafdf101b58e4ee95430c3be25e3e3aa29d7`, parent (fix^1) `daaead9069d562e166918d461191c89773e51f90`
|
||||
- advisory title: Apache PDFBox has Path Traversal through PDComplexFileSpecification.getFilename() function
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `apache-pdfbox-3dc4e739d0307166` | `examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java::try` (66-81) | java | ExtractEmbeddedFiles.main(String[] args) CLI argument args[0] (PDF file path); embedded filename from PDComplexFileSpecification.getFilename() flows into extractFile(filePath, filename, embeddedFile) | 0.9 |
|
||||
| `apache-pdfbox-56ad35de6b5478ab` | `examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java::extractFilesFromPage` (84-104) | java | main(String[] args) CLI: args[0] PDF file path; embedded file names from PDComplexFileSpecification.getFilename() flow into extractFile | 0.9 |
|
||||
| `apache-pdfbox-7d8d6513c163f5fe` | `examples/src/main/java/org/apache/pdfbox/examples/pdmodel/ExtractEmbeddedFiles.java::main` (56-83) | java | ExtractEmbeddedFiles.main(String[] args) CLI argument args[0] (PDF file path); embedded filename from PDComplexFileSpecification.getFilename() in PDF content reaches extractFile | 0.9 |
|
||||
|
||||
## codecov/codecov-python
|
||||
|
||||
- advisory: GHSA-h3qr-fjhm-jphw (injection, CWE-88), origin `osv-extend`, ecosystem pip, licence Apache-2.0
|
||||
- fix `2a80aa434f74feb31242b6f213b75ce63ae97902`, parent (fix^1) `73b1b138ec40912eee6fed02636be6011105706e`
|
||||
- advisory title: Codecov does not sanitize gcov arguments
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `codecov-codecov-python-261567376b9ecd8e` | `codecov/__init__.py::main` (209-805) | python | codecov/__init__.py:main() CLI entry point; attacker-controlled source: --gcov-args / --gcov-exec / --gcov-root CLI arguments (argparse) flowing into cmd string passed to try_to_run (popen) | 0.9 |
|
||||
|
||||
## cyanheads/git-mcp-server
|
||||
|
||||
- advisory: GHSA-3q26-f695-pp76 (injection, CWE-77), origin `osv-extend`, ecosystem npm, licence Apache-2.0
|
||||
- fix `0dbd6995ccdf76ab770b58013034365b2d06c4d9`, parent (fix^1) `f30169ec3a2520990e5467c19ef42ea7d6d9270e`
|
||||
- advisory title: @cyanheads/git-mcp-server vulnerable to command injection in several tools
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `cyanheads-git-mcp-server-07d42f8af21c4e1d` | `src/mcp-server/tools/gitClone/logic.ts::gitCloneLogic` (69-278) | typescript | MCP tool git_clone (gitCloneLogic) invoked via MCP tools/call with attacker-controlled input.targetPath/repositoryUrl/branch reaching child_process.exec | 0.85 |
|
||||
| `cyanheads-git-mcp-server-3c306de22fb76061` | `src/mcp-server/tools/gitInit/logic.ts::gitInitLogic` (63-227) | typescript | MCP tool git_init (gitInitLogic) invoked via tools/call with input.initialBranch | 0.9 |
|
||||
|
||||
## cyface/django-termsandconditions
|
||||
|
||||
- advisory: GHSA-6rmf-cv6p-4h27 (untrusted_destination, CWE-601), origin `osv-extend`, ecosystem pip, licence BSD-3-Clause
|
||||
- fix `03396a1c2e0af95e12a45c5faef7e47a4b513e1a`, parent (fix^1) `c131a1b1daab533ece466f12d79a9ff903c9cacc`
|
||||
- advisory title: Terms and Conditions Module vulnerable to Open Redirect
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `cyface-django-termsandconditions-1b3a86185cb651c3` | `termsandconditions/views.py::form_valid` (154-185) | python | EmailTermsView at url /terms/email; GET parameter returnTo flows via get_initial into form initial, then form_valid sets self.success_url from form.cleaned_data['returnTo'] without validation, causing | 0.85 |
|
||||
| `cyface-django-termsandconditions-387ba4069e5df737` | `termsandconditions/views.py::post` (76-124) | python | AcceptTermsView.post at URL /terms/accept, reading request.POST['returnTo'] | 0.85 |
|
||||
| `cyface-django-termsandconditions-8ee6351ba4d07811` | `termsandconditions/tests.py::test_accept_redirect_safe` (245-259) | python | POST /terms/accept/ with returnTo parameter handled by termsandconditions/views.py returnTo function | 0.85 |
|
||||
| `cyface-django-termsandconditions-95885d31451074da` | `termsandconditions/tests.py::test_accept_redirect_unsafe` (260-270) | python | POST /terms/accept/ with returnTo parameter (termsandconditions/views.py returnTo function) | 0.85 |
|
||||
| `cyface-django-termsandconditions-d51b737f5af17360` | `termsandconditions/views.py::get_initial` (67-75) | python | AcceptTermsView at url /terms/accept; GET parameter returnTo read via self.request.GET.get("returnTo", "/") in get_initial | 0.85 |
|
||||
|
||||
## datamodel-code-generator/datamodel-code-generator
|
||||
|
||||
- advisory: GHSA-77xj-x4rm-935c (path, CWE-22), origin `osv`, ecosystem pip, licence MIT
|
||||
- fix `5e94b8f4203198798ec66b8e48217f70af69a0dc`, parent (fix^1) `834731d56c0a90c182a0919f316069cf5ff0659a`
|
||||
- advisory title: datamodel-code-generator: Protobuf weak-import path traversal allows files to be written outside the temporary directory
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `datamodel-code-generator-datamodel-code-generator-bd146f812470891a` | `src/datamodel_code_generator/parser/protobuf.py::_write_missing_weak_imports` (383-395) | python | CLI: datamodel-code-generator --input-file-type protobuf --input <attacker-controlled .proto> (or --url); source: WEAK_IMPORT_PATTERN capture group from the proto text reaching _write_missing_weak_imp | 0.9 |
|
||||
|
||||
## docling-project/docling
|
||||
|
||||
- advisory: GHSA-p3fw-7699-7926 (config_secrets, CWE-522), origin `osv`, ecosystem pip, licence MIT
|
||||
- fix `5e469137f275ffc443306a30d12a3a45bceb80fb`, parent (fix^1) `efaf9edd61363ef7239a49ba8e6b349ee219268f`
|
||||
- advisory title: Docling: Configured HTTP headers sent to every remote image host named by a document
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `docling-project-docling-442cf60d08eedb5a` | `docling/cli/main.py::convert` (769-1181) | python | CLI command `convert` (docling/cli/main.py) with `--html-image-headers` and `--html-image-fetch remote\|all`; attacker-controlled source is the untrusted HTML document's `<img src>` URL | 0.85 |
|
||||
| `docling-project-docling-6fdb354a87a33a5b` | `docling/backend/html_backend.py::_render_with_browser` (1004-1208) | python | HTMLBackend._render_with_browser via HTMLBackendOptions(enable_remote_fetch=True, fetch_images=True, headers=...) / CLI --html-image-headers with --html-image-fetch remote\|all; attacker-controlled so | 0.75 |
|
||||
| `docling-project-docling-e161c7347b530396` | `docling/backend/html_backend.py::_route_request` (1044-1054) | python | HTMLBackend._route_request via HTMLBackendOptions(enable_remote_fetch=True, fetch_images=True, headers=...) / CLI --html-image-headers with --html-image-fetch remote\|all; attacker-controlled source i | 0.75 |
|
||||
|
||||
## dtwang/line-desktop-mcp
|
||||
|
||||
- advisory: GHSA-4hf8-5mjm-rfgq (access_control, CWE-306), origin `osv`, ecosystem npm, licence MIT
|
||||
- fix `680617894981ea93f8f6ceb51ecde7519754d501`, parent (fix^1) `4023b0c85004507e28ae08999aac689293db63a5`
|
||||
- advisory title: Streamable HTTP mode exposes LINE Desktop read/send tools without MCP authentication
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `dtwang-line-desktop-mcp-11072dcfdfa41a45` | `src/server.js::parseArgs` (575-607) | javascript | CLI --http-mode --port 3000 binds Express app to 0.0.0.0 and exposes POST /mcp Streamable HTTP endpoint without authentication | 0.9 |
|
||||
| `dtwang-line-desktop-mcp-b9429141072345c4` | `src/server.js::run` (438-574) | javascript | Express app.all('/mcp') handler in LineDesktopMCPServer.run() invoked via CLI --http-mode --port; attacker-controlled source is the unauthenticated HTTP POST to /mcp (req.body initialize / mcp-session | 0.9 |
|
||||
|
||||
## flytohub/flyto-core
|
||||
|
||||
- advisory: GHSA-c9hr-64h3-gxpc (untrusted_destination, CWE-918), origin `osv`, ecosystem pip, licence Apache-2.0
|
||||
- fix `0a0a528520ec18f5a21f1ddf858a71cc1edfb6e9`, parent (fix^1) `2471c6e774102fcca21c61eb66cae057c0f0cecf`
|
||||
- advisory title: Flyto2 Core: Guarded HTTP modules follow redirects into internal space without per-hop SSRF revalidation
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `flytohub-flyto-core-9648472ee6b1bd21` | `src/core/modules/atomic/http/request.py::http_request` (280-398) | python | execute_module http.request {url: attacker-controlled} -> http_request() -> session.request(method, url, allow_redirects=True) follows 302 into internal space | 0.85 |
|
||||
| `flytohub-flyto-core-c57a8e72e5f62c6f` | `src/core/modules/atomic/http/get.py::http_get` (85-125) | python | execute_module http.get with attacker-controlled url parameter (http_get in src/core/modules/atomic/http/get.py) | 0.85 |
|
||||
|
||||
## graphql/graphql-playground
|
||||
|
||||
- advisory: GHSA-59r9-6jp6-jcm7 (output_encoding, CWE-79), origin `osv-extend`, ecosystem npm, licence MIT
|
||||
- fix `b8a956006835992f12c46b90384a79ab82bcadad`, parent (fix^1) `5073062f3834bb8652c0c0e3297ad83ddc8ad867`
|
||||
- advisory title: XSS vulnerability in GraphQL Playground from untrusted schemas
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `graphql-graphql-playground-8a960cf0662c724e` | `packages/graphql-playground-react/src/components/Playground/onHasCompletion.tsx::renderType` (136-138) | typescript | GraphQL Playground endpoint query parameter (?endpoint=...) loading a malicious schema; introspection type names reach renderType via onHasCompletion autocomplete, then injected through information.in | 0.85 |
|
||||
| `graphql-graphql-playground-8ab7cfafbc027403` | `packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts::getSchema` (95-166) | typescript | SchemaFetcher.fetchSchema via endpoint query parameter (session.endpoint) loading a malicious schema | 0.75 |
|
||||
| `graphql-graphql-playground-94e027d23fc54155` | `packages/graphql-playground-react/src/components/Playground.tsx::UNSAFE_componentWillReceiveProps` (215-253) | typescript | GraphQL Playground React app loaded via graphql-playground-html/middleware; endpoint query parameter (?endpoint=...) sets the schema endpoint, and the fetched introspection schema flows through props. | 0.75 |
|
||||
| `graphql-graphql-playground-afc7c3fd32fb660f` | `packages/graphql-playground-react/src/components/Playground/onHasCompletion.tsx::onHasCompletion` (15-135) | typescript | GraphQL Playground endpoint query parameter (?endpoint=...) loading a malicious schema; onHasCompletion renders untrusted schema type names via innerHTML | 0.85 |
|
||||
|
||||
## hkuds/vibe-trading
|
||||
|
||||
- advisory: GHSA-5rmq-chc7-m22f (path, CWE-22), origin `osv`, ecosystem pip, licence MIT
|
||||
- fix `9454d4a27a763b80e1d6eb5763b86c88e9e4e714`, parent (fix^1) `fd767f5c29c72cffe8e3f89c7ff006cb3041397f`
|
||||
- advisory title: Vibe-Trading file-read tools expose arbitrary server-readable files
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `hkuds-vibe-trading-16a24d8276343ff1` | `agent/src/tools/path_utils.py::safe_user_path` (52-81) | python | POST /sessions/{sid}/messages -> LLM tool registry -> ExtractShadowStrategyTool (journal_path) -> safe_user_path | 0.85 |
|
||||
| `hkuds-vibe-trading-866d7a8070276364` | `agent/src/tools/doc_reader_tool.py::read_document` (259-291) | python | POST /sessions/{sid}/messages -> LLM tool registry -> DocReaderTool.execute -> read_document(file_path) with attacker-controlled file_path from chat message | 0.85 |
|
||||
| `hkuds-vibe-trading-c16e9e336b3428d7` | `agent/src/tools/backtest_tool.py::run_backtest` (12-64) | python | BacktestTool.execute -> run_backtest(run_dir) via POST /sessions/{sid}/messages LLM tool invocation; run_dir is attacker-controlled via prompt injection | 0.55 |
|
||||
| `hkuds-vibe-trading-c504e5fd82839ef8` | `agent/src/tools/pattern_tool.py::run_pattern` (305-342) | python | POST /sessions/{sid}/messages -> LLM tool registry -> PatternTool.execute -> run_pattern(run_dir) | 0.55 |
|
||||
|
||||
## home-assistant/frontend
|
||||
|
||||
- advisory: GHSA-wx4m-69m9-gx3m (output_encoding, CWE-80), origin `osv`, ecosystem pip, licence Apache-2.0
|
||||
- fix `b8c201b6d34414d30c622797366570185c219614`, parent (fix^1) `4a6c23c93e0cf24c26721090ba168a853b91843f`
|
||||
- advisory title: Home Assistant: XSS in Statistics Graph Card
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `home-assistant-frontend-588ec87d01bd562c` | `src/panels/lovelace/cards/energy/common/energy-chart-options.ts::formatTooltip` (226-299) | typescript | Lovelace Statistics Graph card (statistics-chart.ts) rendering ECharts tooltip via formatTooltip; source is entity name from getStatisticLabel/computeStateName (attacker-controlled via helper or integ | 0.85 |
|
||||
| `home-assistant-frontend-589375de71f6b2f2` | `src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts::customElement` (33-388) | typescript | ZWave JS network visualization panel (zwave_js-network-visualization custom element) tooltip formatter rendering device names via ECharts tooltip | 0.6 |
|
||||
| `home-assistant-frontend-72bcad0f4ba8aa00` | `src/components/chart/state-history-chart-timeline.ts::render` (66-171) | typescript | Statistics Graph card / state-history-chart-timeline.ts _renderTooltip (seriesName from entity name via getStatisticLabel/computeStateName) | 0.75 |
|
||||
| `home-assistant-frontend-d7404ee83aa6f810` | `src/components/chart/ha-chart-base.ts::_getSeries` (955-996) | typescript | Statistics Graph card (statistics-chart.ts) rendering entity name via getStatisticLabel -> computeStateName into ECharts tooltip; attacker-controlled source is the entity name set via helper/integrati | 0.85 |
|
||||
| `home-assistant-frontend-f515548346e64ad5` | `src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts::_renderTooltip` (201-282) | typescript | Lovelace Statistics Graph card tooltip formatter (_renderTooltip) rendering entity name from getStatisticLabel/computeStateName; attacker-controlled source is the entity/statistic name set via helper | 0.85 |
|
||||
|
||||
## htmlunit/htmlunit
|
||||
|
||||
- advisory: GHSA-3xrr-7m6p-p7xh (injection, CWE-94), origin `osv`, ecosystem maven, licence Apache-2.0
|
||||
- fix `641325bbc84702dc9800ec7037aec061ce21956b`, parent (fix^1) `d75f21270a20b94c4a49aa0a7dff3f9b7309ca78`
|
||||
- advisory title: HtmlUnit Code Injection vulnerability
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `htmlunit-htmlunit-2d8f01031e36f7f1` | `src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessor.java::transform` (121-209) | java | XSLTProcessor.transform (JS-exposed @JsxFunction transformToDocument/transformToFragment) invoked from attacker-controlled webpage JavaScript in HtmlUnit browser | 0.75 |
|
||||
|
||||
## inventree/inventree
|
||||
|
||||
- advisory: GHSA-vx3h-qwqw-r2wq (untrusted_destination, CWE-918), origin `osv-extend`, ecosystem pip, licence MIT
|
||||
- fix `5759b60a48e7e178fb417a900ed543f29dc5dc86`, parent (fix^1) `7bffa49f3cdd44dffdcf14e11e57db6295273b36`
|
||||
- advisory title: Inventree Server-Side Request Forgery vulnerability exposes server port/internal IP
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `inventree-inventree-3c189cf58ad5161c` | `src/backend/InvenTree/InvenTree/serializers.py::validate_remote_image` (890-914) | python | RemoteImageMixin.validate_remote_image via DRF serializer field 'remote_image' (URLField) on API endpoints using RemoteImageMixin; attacker-controlled source is the remote_image URL submitted in the r | 0.75 |
|
||||
|
||||
## jenkinsci/ansible-plugin
|
||||
|
||||
- advisory: GHSA-38hw-368m-7jmg (config_secrets, CWE-311), origin `osv`, ecosystem maven, licence Apache-2.0
|
||||
- fix `4cbc48657c21a65a917b3b3049918480198c0cfb`, parent (fix^1) `8191fd551ebf606556c730a08eb1d74426dc6257`
|
||||
- advisory title: Jenkins Ansible Plugin stores and displays secrets in plain text
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `jenkinsci-ansible-plugin-4904f1516a63debe` | `src/main/java/org/jenkinsci/plugins/ansible/workflow/AnsiblePlaybookStep.java::convertExtraVars` (376-397) | java | AnsiblePlaybookStep.AnsiblePlaybookExecution.run() invoked via the ansiblePlaybook pipeline step; extraVars parameter (step.extraVars) is attacker/user-controlled and passed to convertExtraVars | 0.75 |
|
||||
|
||||
## jenkinsci/dependency-track-plugin
|
||||
|
||||
- advisory: GHSA-xfrw-pcmc-r2p3 (access_control, CWE-862), origin `osv-extend`, ecosystem maven, licence Apache-2.0
|
||||
- fix `70e7b82ad9a10499e628998a0bcb57c1481c66bc`, parent (fix^1) `69317029c8a3eaadc7fe3f5a57de2c364279f612`
|
||||
- advisory title: Missing permission checks in Jenkins OWASP Dependency-Track Plugin allow capturing credentials
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `jenkinsci-dependency-track-plugin-673cc48b4f75ebf5` | `src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java::doCheckDependencyTrackFrontendUrl` (205-219) | java | Jenkins Stapler HTTP endpoint doCheckDependencyTrackFrontendUrl (and doTestConnection) invoked via form validation request; attacker-controlled source is the @QueryParameter value / dependencyTrackApi | 0.85 |
|
||||
| `jenkinsci-dependency-track-plugin-90b566be2c3aebff` | `src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java::doCheckDependencyTrackUrl` (195-204) | java | Jenkins HTTP endpoint doCheckDependencyTrackUrl (and doTestConnection) reachable via Stapler form validation; attacker-controlled source: @QueryParameter value / dependencyTrackUrl and dependencyTrack | 0.85 |
|
||||
| `jenkinsci-dependency-track-plugin-9e8488d96f7c0424` | `src/main/java/org/jenkinsci/plugins/DependencyTrack/DescriptorImpl.java::doTestConnection` (220-245) | java | Jenkins Stapler HTTP endpoint doTestConnection (descriptor form validation endpoint, e.g. /descriptorByName/.../testConnection) with attacker-controlled dependencyTrackUrl and dependencyTrackApiKey qu | 0.85 |
|
||||
|
||||
## jenkinsci/gitlab-logo-plugin
|
||||
|
||||
- advisory: GHSA-22rj-q66g-2jg3 (config_secrets, CWE-522), origin `osv-extend`, ecosystem maven, licence MIT
|
||||
- fix `1a64595353df91b5fcf2d9336fa627e06ef1f8a9`, parent (fix^1) `f4c28e3db4974bfb03ba41838e68a1d99debd75b`
|
||||
- advisory title: Jenkins GitLab Logo Plugin stores credentials unencrypted
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `jenkinsci-gitlab-logo-plugin-f771ba20ac33c126` | `src/main/java/org/jenkinsci/plugins/gitlablogo/GitlabLogoProperty.java::configure` (100-110) | java | Jenkins global configuration form submission handled by DescriptorImpl.configure(StaplerRequest, JSONObject) via Stapler; attacker-controlled source is the privateToken form field in the submitted JSO | 0.85 |
|
||||
|
||||
## jenkinsci/job-import-plugin
|
||||
|
||||
- advisory: GHSA-4g29-r7vj-2rpv (access_control, CWE-862), origin `osv-extend`, ecosystem maven, licence MIT
|
||||
- fix `1b4119849571d4879977c529f9972d271ad6a630`, parent (fix^1) `647b3d5a034708c7f8dd61194c476b4e2b8d9593`
|
||||
- advisory title: Jenkins Job Import Plugin allows attackers with Overall/Read permission to enumerate credentials IDs of credentials stored in Jenkins
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `jenkinsci-job-import-plugin-07e0e7023a46cf4d` | `src/main/java/org/jenkins/ci/plugins/jobimport/JobImportAction.java::doFillJenkinsSitesItems` (336-342) | java | Jenkins Stapler HTTP endpoint doFillJenkinsSitesItems on JobImportActionDescriptor (fill field for Jenkins sites), reachable by any authenticated user with Overall/Read permission | 0.85 |
|
||||
| `jenkinsci-job-import-plugin-5509a356384e862e` | `src/main/java/org/jenkins/ci/plugins/jobimport/JobImportAction.java::doFillCredentialIdItems` (318-335) | java | Jenkins Stapler HTTP endpoint /jobImport/fillCredentialIdItems (doFillCredentialIdItems on JobImportActionDescriptor), reachable by any authenticated user with Overall/Read permission | 0.85 |
|
||||
|
||||
## jenkinsci/p4-plugin
|
||||
|
||||
- advisory: GHSA-f9v6-p7hp-c3qx (access_control, CWE-862), origin `osv`, ecosystem maven, licence BSD-2-Clause
|
||||
- fix `2f2a31d8d36de7abab45820fab3a82f4c682b760`, parent (fix^1) `a168eb417f4b725612123be12ac014b01ab09999`
|
||||
- advisory title: Missing permission checks in Jenkins P4 Plugin
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `jenkinsci-p4-plugin-5022afedadfe8449` | `src/main/java/org/jenkinsci/plugins/p4/trigger/P4Hook.java::doChangeSubmit` (105-125) | java | Stapler HTTP endpoint doChangeSubmit (P4Hook) reachable via Jenkins Stapler routing; attacker-controlled source: StaplerRequest parameters (_.p4port) and submitted form | 0.9 |
|
||||
| `jenkinsci-p4-plugin-65a59903d43924ee` | `src/main/java/org/jenkinsci/plugins/p4/trigger/P4Hook.java::doChange` (69-91) | java | P4Hook.doChange (HTTP endpoint /p4/change via StaplerRequest) | 0.9 |
|
||||
| `jenkinsci-p4-plugin-85c3775c29eb0c5c` | `src/main/java/org/jenkinsci/plugins/p4/review/ReviewAction.java::doBuild` (78-125) | java | HTTP endpoint doBuild/doBuildSubmit on ReviewAction (urlName 'review') via StaplerRequest parameters | 0.85 |
|
||||
| `jenkinsci-p4-plugin-a1c0ab5c3582fc3b` | `src/main/java/org/jenkinsci/plugins/p4/review/ReviewAction.java::doBuildSubmit` (70-77) | java | HTTP endpoint /review/buildSubmit (Stapler doBuildSubmit) reachable via Overall/Read authenticated user submitting form data | 0.85 |
|
||||
| `jenkinsci-p4-plugin-fdef12ac487474fb` | `src/main/java/org/jenkinsci/plugins/p4/trigger/P4Hook.java::doEvent` (49-68) | java | UnprotectedRootAction P4Hook at URL /p4, HTTP endpoint doEvent (and doChange) reachable without authentication; attacker-controlled source is the HTTP request body/JSON payload (req.getInputStream) | 0.9 |
|
||||
|
||||
## jenkinsci/tap-plugin
|
||||
|
||||
- advisory: GHSA-24jc-w55j-5p83 (path, CWE-22), origin `osv-extend`, ecosystem maven, licence MIT
|
||||
- fix `8e2a3aa86ce57a98c69e6038e15ae8fe47d08124`, parent (fix^1) `a98a85c961ef983fb2935aa2987587acc2e11e8e`
|
||||
- advisory title: Jenkins TAP Plugin allows Path Traversal
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `jenkinsci-tap-plugin-b1954cc1fc64febe` | `src/main/java/org/tap4j/plugin/TapResult.java::doDownloadAttachment` (306-345) | java | doDownloadAttachment Stapler request handler (Jenkins Stapler route /downloadAttachment) reading request parameter "f" | 0.85 |
|
||||
|
||||
## jenkinsci/zap-pipeline-plugin
|
||||
|
||||
- advisory: GHSA-4c87-9xq5-5c35 (output_encoding, CWE-79), origin `osv-extend`, ecosystem maven, licence MIT
|
||||
- fix `bca4b087c66ead39398f54cdadc27c515e8ede31`, parent (fix^1) `5fc703a3c383eb515909cf0b9dc452ccc38f07d9`
|
||||
- advisory title: Content-Security-Policy protection for user content disabled by Jenkins ZAP Pipeline Plugin
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `jenkinsci-zap-pipeline-plugin-953ab12f1f2d06d9` | `src/main/java/com/vrondakis/zap/ZapAction.java::doDynamic` (61-69) | java | ZapAction.doDynamic (Stapler route /job/<job>/<run>/zap/... via getUrlName() returning ZapArchive.DIRECTORY_NAME), serving DirectoryBrowserSupport content; attacker-controlled source is files in the w | 0.85 |
|
||||
|
||||
## line/armeria
|
||||
|
||||
- advisory: GHSA-8fp4-rp6c-5gcv (path, CWE-22), origin `osv-extend`, ecosystem maven, licence Apache-2.0
|
||||
- fix `e2697a575e9df6692b423e02d731f293c1313284`, parent (fix^1) `a1105519e920b00ee3bc848a5074d943925931ef`
|
||||
- advisory title: Path Traversal in com.linecorp.armeria:armeria
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `line-armeria-143c39cfd2874e4c` | `core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java::appendOneByte` (359-389) | java | HTTP request path parsing in Armeria server (PathAndQuery.appendOneByte) reached via FileService under a path prefix, e.g. GET /files/..%2Fsecrets.txt | 0.75 |
|
||||
| `line-armeria-3196308da59204ff` | `core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java::splitPathAndQuery` (189-230) | java | HTTP request path handling in Armeria server (PathAndQuery.splitPathAndQuery invoked from request path parsing); attacker-controlled source is the request path containing %2F, e.g. /files/..%2Fsecrets | 0.75 |
|
||||
| `line-armeria-fff692dc76424e72` | `core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java::decodePercentsAndEncodeToUtf8` (231-358) | java | HTTP request path parsing in Armeria server (PathAndQuery.parse) invoked via FileService under a route such as /files, attacker-controlled source: request path containing %2F | 0.85 |
|
||||
|
||||
## modoboa/modoboa-dmarc
|
||||
|
||||
- advisory: GHSA-vc42-mgr2-w34r (injection, CWE-91), origin `osv-extend`, ecosystem pip, licence MIT
|
||||
- fix `14c29e0ad9487bdbe4cc0bd1f8bc711285bf9933`, parent (fix^1) `1b1472fc7ef168e19f981f8bb8827e9ed7888e46`
|
||||
- advisory title: Modoboa is vulnerable to an XML External Entity Injection (XXE)
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `modoboa-modoboa-dmarc-7b9ba42b6d644e40` | `modoboa_dmarc/lib.py::import_report` (76-119) | python | import_report_from_email(content) -> import_archive -> import_report; content originates from DMARC report emails sent to the rua address of a domain | 0.85 |
|
||||
|
||||
## posit-dev/py-shiny
|
||||
|
||||
- advisory: GHSA-47c3-hpmg-7j6p (path, CWE-22), origin `osv`, ecosystem pip, licence MIT
|
||||
- fix `1d8ecb46cbc9621b7dc8812111d26692e086b376`, parent (fix^1) `48b4faa0d651476afa84d6423d57503b60716902`
|
||||
- advisory title: Shiny for Python has path traversal in bookmark restore
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `posit-dev-py-shiny-3537c0dd41a40d38` | `shiny/_app.py::_on_root_request_cb` (397-418) | python | HTTP GET / handler _on_root_request_cb reading request.url.query (_state_id_) | 0.9 |
|
||||
| `posit-dev-py-shiny-78e995f1fddd0cb5` | `shiny/bookmark/_restore_state.py::_load_state_qs` (183-221) | python | HTTP request query string `_state_id_` parameter reaching `_load_state_qs` (bookmark restore path) | 0.85 |
|
||||
| `posit-dev-py-shiny-8fd07b2eca54563e` | `shiny/bookmark/_restore_state.py::from_query_string` (110-163) | python | HTTP request query string parsed by RestoreContext.from_query_string via the Shiny bookmark-restore path; attacker-controlled source is the `_state_id_` query-string parameter | 0.85 |
|
||||
|
||||
## shopperlabs/shopper
|
||||
|
||||
- advisory: GHSA-f946-9qp6-vgch (access_control, CWE-285), origin `osv-extend`, ecosystem composer, licence MIT
|
||||
- fix `fcd0c5920588702df5b874f432b1042abd77a50b`, parent (fix^1) `ceaea6a078025d4a7634d2646d27dde3f13b6f58`
|
||||
- advisory title: shopper/framework: Authorization bypass in multiple Livewire admin components
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `shopperlabs-shopper-02254beed509364c` | `packages/admin/src/Livewire/Components/Products/Form/Inventory.php::store` (195-207) | php | Livewire component packages/admin/src/Livewire/Components/Products/Form/Inventory.php store() method, invoked via Livewire request from the admin product inventory form; attacker-controlled source is | 0.85 |
|
||||
| `shopperlabs-shopper-047e62abed0b91fa` | `packages/admin/src/Livewire/Components/Customers/Orders.php::table` (36-123) | php | Livewire component Shopper\Admin\Livewire\Components\Customers\Orders (public $customer property, table()/render()); attacker-controlled source: Livewire client-side property tampering of the unlocked | 0.6 |
|
||||
| `shopperlabs-shopper-052f88a347caf2eb` | `packages/admin/src/Livewire/Components/Orders/OrderNotes.php::leaveNotes` (24-37) | php | Livewire component Shopper\Livewire\Components\Orders\OrderNotes::leaveNotes(), invoked via Livewire update request from the order detail admin panel; attacker-controlled source is the public $notes p | 0.85 |
|
||||
| `shopperlabs-shopper-064f0b5f8937aae5` | `packages/admin/src/Livewire/Pages/Settings/Team/Index.php::table` (77-118) | php | Livewire admin component Settings/Team/Index (table recordActions DeleteAction) reachable by any authenticated panel user | 0.75 |
|
||||
| `shopperlabs-shopper-0fd6e8d87dd0ed82` | `packages/admin/src/Livewire/Pages/Order/Detail.php::markCompleteAction` (114-133) | php | Livewire admin Order Detail component action markCompleteAction (Filament action callable by authenticated low-privilege panel user with read_orders) | 0.85 |
|
||||
| `shopperlabs-shopper-1112cc61420816ff` | `packages/admin/src/Livewire/Pages/Settings/Team/RolePermission.php::save` (199-210) | php | Livewire component Settings/Team/RolePermission::save() invoked via Livewire update request from the admin panel route (shopper settings team role page); attacker-controlled source is the authenticate | 0.85 |
|
||||
| `shopperlabs-shopper-13fdb238d0d8e573` | `packages/admin/src/Livewire/Components/Products/Form/Edit.php::store` (213-231) | php | Livewire component Products\Form\Edit::store() invoked via Livewire update request from the admin product edit form; attacker-controlled source is the Livewire form state (data) submitted by an authen | 0.85 |
|
||||
| `shopperlabs-shopper-22487ec06a8cf50a` | `packages/admin/src/Livewire/Pages/Order/Detail.php::capturePaymentAction` (134-178) | php | Livewire admin component packages/admin/src/Livewire/Pages/Order/Detail.php::capturePaymentAction (Filament action 'capturePayment' invoked by an authenticated low-privilege panel user with read_order | 0.85 |
|
||||
| `shopperlabs-shopper-2405022067cc6870` | `packages/admin/src/Livewire/Pages/Order/Shipments.php::table` (98-213) | php | Livewire admin component packages/admin/src/Livewire/Pages/Order/Shipments.php::table() record actions (markDelivered, edit) invoked via Livewire request by an authenticated low-privilege panel user w | 0.85 |
|
||||
| `shopperlabs-shopper-29811dac402a0d13` | `packages/admin/src/Livewire/Pages/Order/Detail.php::markPaidAction` (88-113) | php | Livewire admin component Order/Detail markPaidAction (Filament action 'markPaid') invoked by an authenticated low-privilege panel user with read_orders permission | 0.85 |
|
||||
| `shopperlabs-shopper-2aeaefee52274ee8` | `packages/admin/src/Livewire/Pages/Order/Detail.php::archiveAction` (179-207) | php | Livewire admin component Order/Detail archiveAction (Filament action 'archive') invoked via Livewire request to the admin order detail page; attacker-controlled source is the authenticated low-privile | 0.85 |
|
||||
| `shopperlabs-shopper-5481a94c34f42fbf` | `packages/admin/src/Livewire/Components/Products/Form/Files.php::store` (54-65) | php | Livewire component packages/admin/src/Livewire/Components/Products/Form/Files.php store() action, invoked via Livewire request to the product edit page; attacker-controlled source is the Livewire acti | 0.85 |
|
||||
| `shopperlabs-shopper-5a3dcf2a6b6b16c8` | `packages/admin/src/Livewire/Components/Products/Form/Shipping.php::store` (56-67) | php | Livewire component Products\Form\Shipping::store() invoked via Livewire update request from the admin product shipping form; attacker-controlled source is the Livewire action call and form state ($thi | 0.85 |
|
||||
| `shopperlabs-shopper-5e6161ace0cb4b97` | `packages/admin/src/Livewire/Pages/Settings/Team/RolePermission.php::createPermissionAction` (151-198) | php | Livewire component Settings/Team/RolePermission::createPermissionAction (admin panel route for role permissions), invoked by an authenticated low-privilege panel user with view_users permission | 0.85 |
|
||||
| `shopperlabs-shopper-61e951b6bcd03186` | `packages/admin/src/Livewire/Pages/Settings/Carriers.php::createCarrierAction` (49-66) | php | Livewire component Shopper\Admin\Livewire\Pages\Settings\Carriers::createCarrierAction (admin panel route rendering the Carriers Livewire component); attacker-controlled source: authenticated low-priv | 0.75 |
|
||||
| `shopperlabs-shopper-7cc69fb293ec0e10` | `packages/admin/src/Livewire/Pages/Collection/Edit.php::store` (134-143) | php | Livewire component packages/admin/src/Livewire/Pages/Collection/Edit.php store() action, invoked via Livewire update request from the admin collection edit page by an authenticated low-privilege panel | 0.75 |
|
||||
| `shopperlabs-shopper-8769d9915b8295a9` | `packages/admin/src/Livewire/Pages/Settings/Team/RolePermission.php::generatePermissionsAction` (90-150) | php | Livewire admin component Settings/Team/RolePermission::generatePermissionsAction (Filament action 'generatePermissions'), invoked by an authenticated panel user with only view_users permission; source | 0.85 |
|
||||
| `shopperlabs-shopper-88497bd11908cf4a` | `packages/admin/src/Livewire/Pages/Settings/Currencies.php::table` (43-130) | php | Livewire component Shopper\Admin\Livewire\Pages\Settings\Currencies (route /settings/currencies) - table() ToggleColumn is_enabled, EditAction edit, and grouped bulk actions enable/disable; attacker-c | 0.75 |
|
||||
| `shopperlabs-shopper-8b7021ee40c51b56` | `packages/admin/src/Livewire/SlideOvers/AddVariant.php::form` (71-203) | php | Livewire component AddVariant (packages/admin/src/Livewire/SlideOvers/AddVariant.php) form() barcode TextInput; attacker-controlled source is the barcode form field submitted via the Livewire save() a | 0.6 |
|
||||
| `shopperlabs-shopper-91023b586e4ab747` | `packages/admin/src/Livewire/Components/Settings/Legal/PolicyForm.php::store` (60-72) | php | Livewire component Settings\Legal\PolicyForm::store() invoked via Livewire update request from the admin panel; attacker-controlled source is the authenticated low-privilege panel user's Livewire requ | 0.75 |
|
||||
| `shopperlabs-shopper-a413505de3d037f8` | `packages/admin/src/Livewire/Pages/Settings/PaymentMethods.php::createPaymentAction` (49-66) | php | Livewire component Shopper\Admin\Livewire\Pages\Settings\PaymentMethods::createPaymentAction (admin panel route for payment methods settings); attacker-controlled source: authenticated low-privilege p | 0.75 |
|
||||
| `shopperlabs-shopper-a82b452965e2ffe5` | `packages/admin/src/Livewire/Components/Products/Form/Seo.php::store` (52-63) | php | Livewire component packages/admin/src/Livewire/Components/Products/Form/Seo.php store() action, invoked via Livewire request to the product SEO form; attacker-controlled source is the Livewire compone | 0.85 |
|
||||
| `shopperlabs-shopper-ab37699016e7189c` | `packages/admin/src/Livewire/SlideOvers/AddProduct.php::form` (84-283) | php | Livewire component packages/admin/src/Livewire/SlideOvers/AddProduct.php::store() invoked via Livewire request to the AddProduct slide-over (form submit wire:target="store"); attacker-controlled sourc | 0.6 |
|
||||
| `shopperlabs-shopper-b37cd0c75b594760` | `packages/admin/src/Livewire/Pages/Product/Variant.php::updateStockAction` (46-81) | php | Livewire component Shopper\Admin\Livewire\Pages\Product\Variant::updateStockAction (admin panel product variant page); attacker-controlled source: Livewire action call with form data (sku, barcode) fr | 0.85 |
|
||||
| `shopperlabs-shopper-cd45e0d6604bb846` | `packages/admin/src/Livewire/Pages/Order/Detail.php::cancelOrderAction` (47-69) | php | Livewire admin component Shopper\Livewire\Pages\Order\Detail::cancelOrderAction invoked via Livewire action call on the order detail page (route shopper.orders.detail); attacker-controlled source is t | 0.85 |
|
||||
| `shopperlabs-shopper-d05ad8c2776691be` | `packages/admin/src/Livewire/Components/Settings/Locations/InventoryForm.php::store` (103-118) | php | Livewire component Settings/Locations/InventoryForm::store() invoked via Livewire update request from the admin panel route shopper.settings.locations; attacker-controlled source is the Livewire form | 0.85 |
|
||||
| `shopperlabs-shopper-d26bdca35566e799` | `packages/admin/src/Livewire/Pages/Settings/PaymentMethods.php::table` (67-112) | php | Livewire component PaymentMethods (Settings/PaymentMethods) table actions: ToggleColumn is_enabled, EditAction edit, DeleteAction delete; invoked via Livewire update endpoint by any authenticated pane | 0.85 |
|
||||
| `shopperlabs-shopper-deb90b541bcac1aa` | `packages/admin/src/Livewire/Pages/Order/Detail.php::startProcessingAction` (70-87) | php | Livewire admin component Order/Detail::startProcessingAction (Filament action 'startProcessing') invoked by an authenticated low-privilege panel user with read_orders permission | 0.85 |
|
||||
| `shopperlabs-shopper-df2391064e3c3332` | `packages/admin/src/Livewire/Pages/Settings/Team/Index.php::createRoleAction` (41-76) | php | Livewire component Shopper\Admin\Livewire\Pages\Settings\Team\Index (route: shopper.settings.team.index) - createRoleAction invoked by any authenticated panel user; source: Livewire action call from c | 0.85 |
|
||||
| `shopperlabs-shopper-f3cb66743db8d5f1` | `packages/admin/src/Livewire/Pages/Customers/Create.php::store` (142-178) | php | Livewire component Customers\Create::store() invoked via admin panel route shopper.customers.create (Livewire POST to /admin/customers/create); attacker-controlled source is the Livewire form state ($ | 0.85 |
|
||||
| `shopperlabs-shopper-fddd0d6c3c4501aa` | `packages/admin/src/Livewire/Pages/Settings/Carriers.php::table` (67-112) | php | Livewire component packages/admin/src/Livewire/Pages/Settings/Carriers.php (table actions: ToggleColumn is_enabled, EditAction, DeleteAction) invoked via Livewire update requests by any authenticated | 0.75 |
|
||||
|
||||
## triggerdotdev/trigger.dev
|
||||
|
||||
- advisory: GHSA-4672-hwv6-gq62 (access_control, CWE-639), origin `osv`, ecosystem npm, licence Apache-2.0
|
||||
- fix `6997aeb05e27d2db47f9eda01fdc8a17c81a1ae0`, parent (fix^1) `cc748422d8188833948035281cc50ef4f7ab77e2`
|
||||
- advisory title: Trigger.dev: Cross-environment deployment cancel
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `triggerdotdev-trigger.dev-0eb72665259b13c2` | `apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts::action` (14-75) | typescript | POST /api/v1/schedules/:scheduleId/deactivate (action in apps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.ts), authenticated via authenticateApiRequest using an environment-scoped secret | 0.55 |
|
||||
| `triggerdotdev-trigger.dev-124d417d0ae18fbe` | `apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts::action` (14-75) | typescript | POST /api/v1/schedules/:scheduleId/activate (action in apps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.ts), authenticated via authenticateApiRequest using an environment-scoped secret API | 0.55 |
|
||||
| `triggerdotdev-trigger.dev-2240070f2c86924b` | `apps/webapp/app/v3/services/initializeDeployment.server.ts::call` (24-315) | typescript | POST /api/v1/deployments/:deploymentId/cancel (api.v1.deployments.$deploymentId.cancel.ts) -> DeploymentService.cancelDeployment -> getDeployment(projectId, friendlyId); attacker-controlled source: de | 0.75 |
|
||||
| `triggerdotdev-trigger.dev-30dec4b7031de737` | `apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts::action` (12-78) | typescript | POST /api/v1/deployments/:deploymentId/cancel (action in apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts); attacker-controlled source: API key in request headers via authenticateRequ | 0.9 |
|
||||
| `triggerdotdev-trigger.dev-5b0c0ef74fdf5bc3` | `apps/webapp/app/v3/services/deployment.server.ts::generateRegistryCredentials` (280-333) | typescript | POST /api/v1/deployments/:deploymentId/cancel (apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts) with attacker-controlled secret API key (dev/preview env) and deploymentId param | 0.85 |
|
||||
| `triggerdotdev-trigger.dev-5f1cf28e3ac84980` | `apps/webapp/app/v3/services/deployment.server.ts::getDeployment` (475-515) | typescript | POST /api/v1/deployments/:deploymentId/cancel (apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts) -> DeploymentService.cancelDeployment -> getDeployment; attacker-controlled source: au | 0.9 |
|
||||
| `triggerdotdev-trigger.dev-eebfca9646e01b1c` | `apps/webapp/app/v3/services/deployment.server.ts::cancelDeployment` (193-270) | typescript | POST /api/v1/deployments/$deploymentId/cancel (apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts) -> DeploymentService.cancelDeployment; attacker-controlled source: authenticated envir | 0.9 |
|
||||
|
||||
## whiskeysockets/baileys
|
||||
|
||||
- advisory: GHSA-qvv5-jq5g-4cgg (config_secrets, CWE-346), origin `osv`, ecosystem npm, licence MIT
|
||||
- fix `3beb08eecfcb4e65722e674034bd84fb11a9de35`, parent (fix^1) `28ca087cf1cbe3c05a5b704d67b3e2270d647b74`
|
||||
- advisory title: Baileys has message upsert / hist sync spoofing and app state corruption when using maliciously crafted protocolMessage payload
|
||||
|
||||
| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |
|
||||
|---|---|---|---|---|
|
||||
| `whiskeysockets-baileys-89cd6d4edf46d286` | `src/Utils/process-message.ts::processMessage` (293-751) | typescript | processMessage(message, ctx) invoked from the Baileys socket message pipeline (e.g. sendMessage/ev 'messages.upsert' handling); attacker-controlled source is the incoming WAMessage.message.protocolMes | 0.75 |
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# Training set for the hunter learning loop: sourcing run (2026-10-13)
|
||||
|
||||
Spec task 4.1 of `.kiro/specs/hunter-learning-loop/` (requirements R2.1 to R2.3). This is the record of the
|
||||
sourcing run. **The training set is not frozen yet.** The model checks are done; the maintainer's spot-check is
|
||||
not, and `assemble` only freezes accepted locations. Even if every candidate is accepted, the set has **28
|
||||
positive changes, short of the 40 R2.3 asks for.** A run on it is a dry run until more positives are sourced.
|
||||
|
||||
## Files
|
||||
|
||||
In the tree (`benchmarks/valid/train/`):
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `sourcing.py` | Every step of this run (`used`, `sources`, `clone`, `filter`, `overlap`, `merge`, `verify`, `candidates`, `precheck`) |
|
||||
| `sources.json` | 163 advisories that survived every exclusion, in the pilot's `sources.json` shape plus `origin`, `ecosystem`, `license_spdx` |
|
||||
| `catalog.toml` | 413 fix-site locations for those advisories (test, vendored and minified files removed) |
|
||||
| `verification.json` | One verifier record per catalog location (all `rejected` or `pending_human`) |
|
||||
| `CANDIDATES.md` | The 99 locations that passed all three model checks, by repository, for the spot-check |
|
||||
|
||||
Out of the tree (`~/ousast-results/valid/train/`): `hist/<owner>__<name>` clones, `repos_hist.json`, and
|
||||
`work/`, which holds the exclusion lists (`used.json`, `used_strict.json`, `named_elsewhere.json`), the per-batch
|
||||
sources, catalogs and verifications, the drop lists, the clone log and the run logs. These stay out of the tree on
|
||||
purpose: the repository guards in `tests/` scan `benchmarks/` for repository names, and an exclusion list is by
|
||||
construction a list of names used elsewhere.
|
||||
|
||||
## Counts at every step
|
||||
|
||||
**Candidates.** Three batches, all with seed 1, in the order the task gave:
|
||||
|
||||
| Origin | Sourced | Dropped by the overlap check | Kept |
|
||||
|---|---|---|---|
|
||||
| `pool-fresh` (permissive `pool-p1` entries the pilot never sourced) | 17 | 17 | 0 |
|
||||
| `pool-retry` (pilot-sourced, verifier rejected, not held out) | 20 | 20 | 0 |
|
||||
| `osv` (batch 1, OSV dump) | 75 | 11 | 64 |
|
||||
| `osv-extend` (batches 2 and 3, same order continued) | 100 | 1 | 99 |
|
||||
| **Total** | **212** | **49** | **163** |
|
||||
|
||||
OSV source: the public bulk dumps (`osv-vulnerabilities.storage.googleapis.com/<ecosystem>/all.zip`, no token)
|
||||
for PyPI, npm, Maven and Packagist. Go was left out because the catalog and the function cutter do not read Go
|
||||
fix sites. The filter is `benchmarks/unseen/source.py:candidate()`, run unchanged: GitHub-reviewed, not
|
||||
withdrawn, published 2019 or later, exactly one taxonomy family from the CWEs, exactly one GitHub fix-commit
|
||||
reference, public repository under 500 MB, fix with exactly one parent, and a permissive licence read at the
|
||||
fix's parent (unknown licences are skipped). One advisory per repository. Over the three batches the GitHub
|
||||
checks rejected 306 advisories for a non-permissive or unknown licence, 71 for an ambiguous version bracket, 74
|
||||
for a merge or root fix, 64 for the repository check, 2 not found, and 4 for transient `http_failure`. GitHub
|
||||
calls: 584, 710 and 593 for the three batches (the token came from `gh auth token`). A first run of batch 1 read
|
||||
only npm and Maven, because OSV calls the other two ecosystems `PyPI` and `Packagist` while the shared code
|
||||
expects GitHub's `pip` and `composer`. The adapter maps the names now, and batch 1 was re-run before anything
|
||||
was cloned.
|
||||
|
||||
Kept, by ecosystem: npm 52, Maven 49, pip 41, Composer 21.
|
||||
|
||||
**Overlap check (pilot and corpus exclusion).** A candidate is dropped if anything in the development tree
|
||||
already names its repository. That means `github.com/<name>` or a `repo = "<name>"` line anywhere under
|
||||
`benchmarks/`, `src/`, `tests/`, `plane/` or `.kiro/` (vendored trees skipped, as the pair guard skips them),
|
||||
or a name in the strict used set (`build_used(root)` with every label). This removed:
|
||||
|
||||
- all 17 `pool-fresh` and all 20 `pool-retry`: they are reserved `pool-p1` repositories, and the retries are
|
||||
also named by the pilot's measurement files;
|
||||
- 11 `osv`: hkuds/lightrag, jenkinsci/job-config-history-plugin, decolua/9router, code16/sharp,
|
||||
chainlit/chainlit, mineadmin/mineadmin, netflix/lemur, pheditor/pheditor, mockoon/mockoon and
|
||||
xorbitsai/inference (named in `benchmarks/independent` or `benchmarks/unseen`), plus startreedata/mcp-pinot,
|
||||
whose advisory text links jlowin/fastmcp, a repository in the `advisory-fixes-2` and `-3` pair corpora;
|
||||
- 1 `osv-extend`: googleapis/google-oauth-java-client (named in `benchmarks/unseen`).
|
||||
|
||||
The first `used` step built a looser set that left out free text under `benchmarks/measurements` and
|
||||
`benchmarks/independent`. Pool and population repositories got through that set into the first sourcing pass,
|
||||
and the overlap check removed them afterwards. The merge step also dropped 3 catalog locations whose code
|
||||
comments link a repository named elsewhere: jenkinsci/pipeline-maven-plugin (links jenkinsci/jenkins, and this
|
||||
was its only passing location), snapappointments/bootstrap-select (links angular/angular) and
|
||||
livehelperchat/livehelperchat (links axios/axios); the last two had been rejected anyway.
|
||||
|
||||
**Pilot exclusion check.** None of the 12 held-out pilot repositories appears in `sources.json`, `catalog.toml`,
|
||||
`verification.json` or `CANDIDATES.md`: the overlap is 0, and `sourcing.py` asserts it. None of the 163 kept
|
||||
repositories appears in the strict used set either.
|
||||
|
||||
**Clones.** 212 of 212 cloned with `--single-branch --no-tags` and full first-parent history. Each fix was
|
||||
present or fetched, and the parent was read from git as `fix^1`; 0 of 212 differed from the stored parent. The
|
||||
disk guard fired once: free space reached 6.0 GB after 64 clones, so cloning stopped. I then emptied the working
|
||||
trees with `git sparse-checkout set --no-cone /__worktree_emptied_for_disk__`, which freed 3.4 GB. Nothing
|
||||
downstream reads a checkout; `git sparse-checkout disable` restores one. Cloning then resumed with
|
||||
`--no-checkout`. Free space later fell to 4.8 GB while the engine replay was writing, so clones of repositories
|
||||
that could no longer contribute a positive were deleted (no catalog site, then no passing site, then not a
|
||||
candidate after the overlap check). What is left: 29 clones, 2.4 GB, of which 28 are candidate repositories plus
|
||||
pipeline-maven-plugin, whose only passing site was dropped. Peak use was about 6.5 GB. Host free space at the
|
||||
end: 8.1 GB.
|
||||
|
||||
**Catalog.** Sites before filtering, then after:
|
||||
|
||||
| Batch | Sources | Raw sites | Test / vendored / minified dropped | Kept | Sources with a site |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 112 | 463 | 112 / 5 / 1 | 345 | 75 |
|
||||
| 2 | 60 | 217 | 74 / 0 / 2 | 141 | 34 |
|
||||
| 3 | 40 | 111 | 38 / 3 / 3 | 67 | 30 |
|
||||
|
||||
Three sources hung in the catalog's regex function parser on large PHP files and were skipped after a 90 to 280 s
|
||||
cap: tribalsystems/zenario, getgrav/grav-plugin-admin and genixcms/genixcms. After the overlap check and the
|
||||
merge, the tree catalog holds **413 locations across 96 advisories and repositories**: Java 114, Python 104,
|
||||
TypeScript 94, JavaScript 54, PHP 47.
|
||||
|
||||
`benchmarks/valid/catalog.py` now maps `.java` and `.php`. Before, it only read Python, JavaScript and
|
||||
TypeScript, although `extract.fix_sites` and `function_bounds` read all five. The scope test in
|
||||
`tests/test_valid_benchmark.py` now expects `.php` to give `php` and checks `.rb` as the out-of-scope suffix.
|
||||
|
||||
**Verification.** `deepseek-flash` through the shipped `verify_case`, temperature 0, 1200-token output cap.
|
||||
|
||||
- Calls: **553 of the 600 budget** (345 + 141 + 67), with 0 provider errors and no exception types seen. Every
|
||||
call logs the function bytes read on both sides first, so no verdict comes from an unread function.
|
||||
- Locations ordered round-robin by advisory, so one advisory with many sites could not use up the budget.
|
||||
- On the 413 locations in the tree: 306 vulnerable before the fix, 259 where the fix addresses it (258 both),
|
||||
107 reachable from an in-repo entry point, **99 passing all three with an entry point**.
|
||||
- Across all 553 calls (including dropped candidates): 433, 371 and 143 per check; 132 passing.
|
||||
|
||||
**Candidates for the spot-check (`CANDIDATES.md`).** 99 locations, 28 repositories, 28 positive changes (one
|
||||
fix commit each).
|
||||
|
||||
- Repositories by language: Java 10, Python 9, TypeScript 7, JavaScript 2, PHP 1 (apache/atlas has both
|
||||
JavaScript and TypeScript sites).
|
||||
- Families: access_control 7, path 6, output_encoding 4, injection 4, config_secrets 4,
|
||||
untrusted_destination 3.
|
||||
- Origin: 13 from batch 1 (`osv`), 15 from batches 2 and 3 (`osv-extend`).
|
||||
|
||||
## What blocks the freeze
|
||||
|
||||
1. **Human acceptance.** `assemble.freeze` only takes `accepted` verdicts, so I stopped before assemble. No
|
||||
spot-check was run.
|
||||
2. **Too few positives.** At most 28 changes, against the 40 R2.3 asks for. The yield was about 18% of sourced
|
||||
advisories (29 of 163 survivors had a passing site before the last drop). Reaching 40 accepted needs roughly
|
||||
70 to 100 more advisories, about 200 to 250 more verifier calls, and about 3 GB of transient clone space.
|
||||
With this run's 47 remaining calls, that is a new budget decision for the maintainer.
|
||||
3. **Ordinary-push availability.** A pre-check of `extract.ordinary_history`, the function `assemble` uses, on
|
||||
the first two repositories alphabetically found two failures. Both stop the whole freeze (`freeze` raises):
|
||||
- andrea9293/mcp-documentation-server has 8 usable ordinary commits, short of the 20 needed;
|
||||
- apache/atlas fails with `Rejected: function_parse` after 456 s (a Python file in its history does not parse).
|
||||
|
||||
dtwang/line-desktop-mcp (56 first-parent commits) and modoboa/modoboa-dmarc (87) are also likely short. I
|
||||
stopped the pre-check there: it parses every changed file of every first-parent commit, and five candidates
|
||||
have 7,000 to 22,600 commits (trigger.dev, inventree, apache/pdfbox, home-assistant/frontend,
|
||||
htmlunit/htmlunit). That would be hours of CPU next to the running replay. Expect `assemble` to be slow on
|
||||
those repositories, or reject them in the spot-check.
|
||||
|
||||
## What the maintainer runs after accepting
|
||||
|
||||
Spot-check each location in `CANDIDATES.md`:
|
||||
|
||||
```
|
||||
.venv/bin/python -m benchmarks.valid.verify spot-check benchmarks/valid/train/verification.json <id> accept|reject --reviewer mc --date YYYY-MM-DD
|
||||
```
|
||||
|
||||
Then freeze with ratio 20 and seed 1. The used set must be the pre-run snapshot `work/used_strict.json` plus the
|
||||
12 pilot repositories. A fresh `build_used(root)` would now sweep `benchmarks/valid/train/` itself and drop every
|
||||
training repository.
|
||||
|
||||
```
|
||||
PYTHONPATH=. .venv/bin/python - <<'PY'
|
||||
import json, tomllib
|
||||
from pathlib import Path
|
||||
from benchmarks.valid import assemble
|
||||
from benchmarks.valid.catalog import local_git
|
||||
H = Path.home() / "ousast-results/valid/train"
|
||||
T = Path("benchmarks/valid/train")
|
||||
used = set(json.loads((H / "work/used_strict.json").read_text()))
|
||||
used |= set(json.loads(Path("benchmarks/measurements/2026-10-12-valid-benchmark-pilot/accepted_repos.json").read_text()))
|
||||
repos = {k: local_git(Path(v)) for k, v in json.loads((H / "repos_hist.json").read_text()).items()}
|
||||
record = assemble.freeze(H / "work/freeze", cases=tomllib.loads((T / "catalog.toml").read_text())["case"],
|
||||
verifications=json.loads((T / "verification.json").read_text())["cases"],
|
||||
repos=repos, used=used, seed=1, ratio=20)
|
||||
(T / "pool-train.toml").write_bytes((H / "work/freeze/pool-valid.toml").read_bytes())
|
||||
(T / "freeze-train.json").write_bytes((H / "work/freeze/freeze-valid.json").read_bytes())
|
||||
print(json.dumps({k: record[k] for k in ("freeze_digest", "dropped_count", "label_check_counts")}))
|
||||
PY
|
||||
```
|
||||
|
||||
`dropped_count` should be 0. A non-zero count means an accepted repository is in the used snapshot, which the
|
||||
overlap check should have prevented.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```
|
||||
B=benchmarks/valid/train/sourcing.py; W=~/ousast-results/valid/train/work
|
||||
python $B used
|
||||
python $B sources --osv-dir <dir with PyPI/npm/Maven/Packagist all.zip> --target 75 --seed 1 # batch 1
|
||||
python $B sources --osv-dir ... --target 60 --seed 1 --extend # batch 2, then 40 for batch 3
|
||||
python $B clone [--sources <batch file>]
|
||||
python -m benchmarks.valid.catalog <batch sources> --repos $W/repos_hist.json --out <raw.toml> # per source, with a time cap
|
||||
python $B filter <raw.toml> --out <catalog.toml>
|
||||
DEEPSEEK_API_KEY=... python $B verify <catalog.toml> --repos $W/repos_hist.json --out <verification.json> --budget <n>
|
||||
python $B overlap sources_batch1.json sources_batch2.json sources_batch3.json
|
||||
python $B merge --catalogs catalog_batch{1,2,3}.toml --verifications verification_batch{1,2,3}.json
|
||||
python $B candidates benchmarks/valid/train/verification.json benchmarks/valid/train/catalog.toml --out benchmarks/valid/train/CANDIDATES.md
|
||||
```
|
||||
|
||||
The verifier ran before the overlap check, so 137 of the 553 calls went to locations that were dropped later
|
||||
(134 by the overlap check, 3 by the merge). A fresh run should call `overlap` before `clone`.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,833 @@
|
||||
"""Sourcing run for the hunter-learning-loop TRAINING set (spec task 4.1, requirements R2.1-R2.3).
|
||||
|
||||
Subcommands, in the order they ran on 2026-10-13:
|
||||
|
||||
used the training-only used set (build_used minus measurements/populations; sweep without measurement text)
|
||||
sources candidates: pool-fresh, pool-retry, then OSV (GHSA with one GitHub fix commit, permissive licence)
|
||||
clone single-branch clones with history, fix + fix^1 fetched, parent read from git; disk guard
|
||||
filter drop test, fixture, vendored and minified sites from the catalog
|
||||
verify budgeted verifier run (counts calls, records provider errors by exception type only)
|
||||
candidates CANDIDATES.md: every site that passed all three model checks, for the maintainer's spot-check
|
||||
precheck ordinary-commit availability per repository with a passing site (what assemble will need)
|
||||
|
||||
Everything is deterministic given the seed and the OSV dump; network calls are counted and logged.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
import zipfile
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
PILOT = ROOT / "benchmarks/measurements/2026-10-12-valid-benchmark-pilot"
|
||||
POOL = ROOT / "benchmarks/unseen/pool-p1.toml"
|
||||
HIST = Path.home() / "ousast-results/valid/train/hist"
|
||||
# Intermediates, exclusion lists and per-batch files stay out of the tree: the repository guards in tests/ scan
|
||||
# benchmarks/ for repository names, and an exclusion list is by construction a list of names used elsewhere.
|
||||
WORK = Path.home() / "ousast-results/valid/train/work"
|
||||
OSV_ECOSYSTEMS = {"PyPI": "pip", "npm": "npm", "Maven": "maven", "Packagist": "composer"}
|
||||
MIN_FREE_BYTES = 6 * 1024**3
|
||||
TEST_PATH = re.compile(
|
||||
r"(^|/)(tests?|__tests__|spec|specs|fixtures?|testdata|e2e|__mocks__|mocks?)(/|$)|\.(test|spec)\.[cm]?[jt]sx?$|(^|/)test_[^/]*\.py$|_test\.py$|Test\.java$|Tests?\.php$",
|
||||
re.I,
|
||||
)
|
||||
VENDORED_PATH = re.compile(r"(^|/)(node_modules|vendor|dist|build|third[_-]party|bower_components)(/|$)", re.I)
|
||||
MINIFIED_PATH = re.compile(r"\.min\.[cm]?js$|\.bundle\.js$", re.I)
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
print(message, flush=True)
|
||||
|
||||
|
||||
def free_bytes(path: Path) -> int:
|
||||
usage = shutil.disk_usage(path)
|
||||
return usage.free
|
||||
|
||||
|
||||
def git(path: Path, *args: str, check: bool = True, timeout: int = 1800) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
["git", "-c", "credential.helper=", *args],
|
||||
cwd=path,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
timeout=timeout,
|
||||
env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
|
||||
)
|
||||
|
||||
|
||||
def pilot_accepted() -> set[str]:
|
||||
return set(json.loads((PILOT / "accepted_repos.json").read_text()))
|
||||
|
||||
|
||||
def pilot_sources() -> list[dict]:
|
||||
return json.loads((PILOT / "sources.json").read_text())
|
||||
|
||||
|
||||
def pool_public() -> list[dict]:
|
||||
rows = tomllib.loads(POOL.read_text())["repository"]
|
||||
return [row for row in rows if row.get("license_class") == "permissive"]
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- used
|
||||
|
||||
|
||||
def cmd_used(args) -> int:
|
||||
from benchmarks.unseen.eligibility import build_used, excluded, repositories
|
||||
|
||||
used = build_used(ROOT)
|
||||
strict = set().union(*(names for label, names in used.sources.items() if label not in {"measurements", "populations"}))
|
||||
# The sweep label reads benchmarks/measurements and the pilot's provenance as free text, which is the
|
||||
# circularity NOTES.md names: a repository is not "used for training" because a measurement wrote its
|
||||
# URL down. The refined set keeps every training label and sweeps only the tool's code, tests, plane
|
||||
# and specs, not measurement records.
|
||||
refined = set().union(*(names for label, names in used.sources.items() if label not in {"measurements", "populations", "sweep"}))
|
||||
sweep_roots = [ROOT / top for top in ("src", "tests", "plane", ".kiro")] + [ROOT / "benchmarks"]
|
||||
skip = (
|
||||
ROOT / "benchmarks/unseen",
|
||||
ROOT / "benchmarks/measurements",
|
||||
ROOT / "benchmarks/experiments",
|
||||
ROOT / "benchmarks/valid",
|
||||
ROOT / "benchmarks/independent",
|
||||
)
|
||||
files = 0
|
||||
for top in sweep_roots:
|
||||
for path in top.rglob("*"):
|
||||
if not path.is_file() or excluded(path) or any(path.is_relative_to(s) for s in skip):
|
||||
continue
|
||||
files += 1
|
||||
refined.update(repositories(path.read_bytes().decode("utf-8", errors="ignore"), {}))
|
||||
record = {
|
||||
"labels": {label: len(names) for label, names in used.sources.items()},
|
||||
"strict_count": len(strict),
|
||||
"refined_count": len(refined),
|
||||
"refined_sweep_files": files,
|
||||
"refined_sweep_skips": [str(s.relative_to(ROOT)) for s in skip],
|
||||
"pilot_repos_in_refined": sorted(pilot_accepted() & refined),
|
||||
}
|
||||
(WORK / "used_strict.json").write_text(json.dumps(sorted(strict), indent=0) + "\n")
|
||||
(WORK / "used.json").write_text(json.dumps(sorted(refined), indent=0) + "\n")
|
||||
(WORK / "used_log.json").write_text(json.dumps(record, indent=2, sort_keys=True) + "\n")
|
||||
log(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- sources
|
||||
|
||||
|
||||
def osv_text(record: dict) -> str:
|
||||
summary = (record.get("summary") or "").strip()
|
||||
details = (record.get("details") or "").strip()
|
||||
return (summary + "\n\n" + details).strip() if summary else details
|
||||
|
||||
|
||||
def cwe_for(record: dict, family: str, taxonomy) -> str | None:
|
||||
for cwe in (record.get("database_specific") or {}).get("cwe_ids", []):
|
||||
found = taxonomy.family_of_cwe(cwe)
|
||||
if found and found.id == family:
|
||||
return cwe
|
||||
return None
|
||||
|
||||
|
||||
def github_row(record: dict) -> dict:
|
||||
"""An OSV GHSA record in the shape source.candidate expects (GitHub advisory API, so GitHub ecosystem names:
|
||||
OSV's PyPI and Packagist become pip and composer)."""
|
||||
specific = record.get("database_specific") or {}
|
||||
return {
|
||||
"ghsa_id": record["id"],
|
||||
"published_at": record["published"],
|
||||
"type": "reviewed" if specific.get("github_reviewed") else "unreviewed",
|
||||
"withdrawn_at": record.get("withdrawn"),
|
||||
"cwes": [{"cwe_id": c} for c in specific.get("cwe_ids", [])],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"package": {
|
||||
"ecosystem": OSV_ECOSYSTEMS.get(a["package"]["ecosystem"], a["package"]["ecosystem"]),
|
||||
"name": a["package"]["name"],
|
||||
}
|
||||
}
|
||||
for a in record.get("affected", [])
|
||||
if a.get("package")
|
||||
],
|
||||
"references": [r.get("url", "") for r in record.get("references", [])],
|
||||
}
|
||||
|
||||
|
||||
class ZipOSV:
|
||||
"""source.OSV look-alike that answers from the downloaded dump (no network)."""
|
||||
|
||||
def __init__(self, records: dict[str, dict]):
|
||||
self.records = records
|
||||
self.calls = 0
|
||||
|
||||
def get(self, identifier: str) -> dict:
|
||||
self.calls += 1
|
||||
return self.records.get(identifier, {})
|
||||
|
||||
|
||||
def cmd_sources(args) -> int:
|
||||
from benchmarks.unseen import source
|
||||
from benchmarks.unseen.eligibility import RepositoryNotFound, normalize
|
||||
from openultrasast.model.taxonomy import load_families
|
||||
|
||||
taxonomy = load_families()
|
||||
accepted = pilot_accepted()
|
||||
pilot = pilot_sources()
|
||||
pilot_advisories = {row["advisory"] for row in pilot}
|
||||
pilot_repos = {normalize(row["repository"]) for row in pilot}
|
||||
public = pool_public()
|
||||
pool_repos = {normalize(row["repository"]) for row in public}
|
||||
used = set(json.loads((WORK / "used.json").read_text()))
|
||||
excluded_repos = accepted | pilot_repos | pool_repos | used
|
||||
log(
|
||||
f"exclusion set: pilot accepted {len(accepted)}, pilot sourced {len(pilot_repos)}, pool public {len(pool_repos)}, used {len(used)} -> {len(excluded_repos)} names" # noqa: E501
|
||||
)
|
||||
|
||||
osv_api = source.OSV()
|
||||
rows: list[dict] = []
|
||||
counts: Counter = Counter()
|
||||
if args.extend:
|
||||
# Second batch: only OSV, continuing the same seeded order past every repository already sourced.
|
||||
earlier = json.loads((WORK / "sources.json").read_text())
|
||||
excluded_repos |= {normalize(row["repository"]) for row in earlier}
|
||||
log(f"extend: {len(earlier)} earlier sources excluded")
|
||||
|
||||
# (a) pool-fresh: permissive pool entries the pilot never sourced
|
||||
for row in [] if args.extend else public:
|
||||
repo = normalize(row["repository"])
|
||||
if repo in accepted or row["advisory"] in pilot_advisories or repo in pilot_repos:
|
||||
continue
|
||||
record = osv_api.get(row["advisory"])
|
||||
cwe = cwe_for(record, row["family"], taxonomy)
|
||||
if not record or not cwe:
|
||||
counts["pool-fresh:no_osv_record_or_cwe"] += 1
|
||||
log(f"pool-fresh skip {row['advisory']}: no OSV record or no CWE in family {row['family']}")
|
||||
continue
|
||||
rows.append(
|
||||
dict(
|
||||
repository=repo,
|
||||
repo=repo,
|
||||
url=row["url"],
|
||||
fix=row["fix"],
|
||||
parent=row["parent"],
|
||||
family=row["family"],
|
||||
cwe=cwe,
|
||||
advisory=row["advisory"],
|
||||
advisory_text=osv_text(record),
|
||||
ecosystem=row["ecosystem"],
|
||||
license_spdx=row.get("license_spdx"),
|
||||
origin="pool-fresh",
|
||||
)
|
||||
)
|
||||
counts["pool-fresh"] += 1
|
||||
|
||||
# (b) pool-retry: the pilot sourced them, no site passed, repository not held out
|
||||
for row in [] if args.extend else pilot:
|
||||
repo = normalize(row["repository"])
|
||||
if repo in accepted:
|
||||
continue
|
||||
rows.append({**row, "repository": repo, "repo": repo, "origin": "pool-retry"})
|
||||
counts["pool-retry"] += 1
|
||||
|
||||
# (c) OSV dump: GHSA advisories with exactly one GitHub fix commit, one family, repo not excluded
|
||||
pre: dict[str, dict] = {}
|
||||
records: dict[str, dict] = {}
|
||||
for eco_name, eco in OSV_ECOSYSTEMS.items():
|
||||
archive = zipfile.ZipFile(Path(args.osv_dir) / f"{eco_name}.zip")
|
||||
names = [n for n in archive.namelist() if n.startswith("GHSA-") and n.endswith(".json")]
|
||||
counts[f"osv:{eco}:ghsa_records"] += len(names)
|
||||
for name in names:
|
||||
record = json.loads(archive.read(name))
|
||||
row = github_row(record)
|
||||
try:
|
||||
if date.fromisoformat(row["published_at"][:10]) < date(2019, 1, 1):
|
||||
raise source.Rejected("date")
|
||||
if row["type"] != "reviewed" or row.get("withdrawn_at"):
|
||||
raise source.Rejected("advisory_status")
|
||||
if source.ecosystem(row) != eco:
|
||||
raise source.Rejected("ecosystem")
|
||||
families = {f.id for c in row["cwes"] if (f := taxonomy.family_of_cwe(c["cwe_id"])) and f.id in source.FAMILIES}
|
||||
if len(families) != 1:
|
||||
raise source.Rejected("family")
|
||||
links = source.fix_links(row)
|
||||
if len(links) != 1:
|
||||
raise source.Rejected("fix_links")
|
||||
((repo, _),) = links
|
||||
if repo in excluded_repos:
|
||||
raise source.Rejected("excluded_repository")
|
||||
except source.Rejected as exc:
|
||||
counts[f"osv:{eco}:{exc}"] += 1
|
||||
continue
|
||||
counts[f"osv:{eco}:prefilter_pass"] += 1
|
||||
records[record["id"]] = record
|
||||
# one advisory per repository: the most recently published one
|
||||
current = pre.get(repo)
|
||||
if current is None or row["published_at"] > current["published_at"]:
|
||||
pre[repo] = {**row, "_eco": eco}
|
||||
log(f"OSV prefilter: {len(pre)} repositories with a candidate advisory")
|
||||
counts["osv:prefilter_repositories"] = len(pre)
|
||||
|
||||
token = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True, check=True).stdout.strip()
|
||||
github = source.GitHub(token)
|
||||
osv_stub = ZipOSV(records)
|
||||
ordered = source.ordered_advisories(list(pre.values()), args.seed)
|
||||
target = args.target
|
||||
picked = 0
|
||||
for row in ordered:
|
||||
if picked >= target:
|
||||
break
|
||||
try:
|
||||
cand = source.candidate(row, github, osv_stub)
|
||||
except source.Rejected as exc:
|
||||
counts[f"osv:check:{exc}"] += 1
|
||||
continue
|
||||
except RepositoryNotFound:
|
||||
counts["osv:check:not_found"] += 1
|
||||
continue
|
||||
except ValueError as exc:
|
||||
counts[f"osv:check:{type(exc).__name__}:{exc}"] += 1
|
||||
continue
|
||||
if cand.license_class != "permissive":
|
||||
counts["osv:check:licence_not_permissive"] += 1
|
||||
continue
|
||||
if cand.repository in excluded_repos:
|
||||
counts["osv:check:excluded_after_resolution"] += 1
|
||||
continue
|
||||
record = records[row["ghsa_id"]]
|
||||
cwe = cwe_for(record, cand.family, taxonomy)
|
||||
rows.append(
|
||||
dict(
|
||||
repository=cand.repository,
|
||||
repo=cand.repository,
|
||||
url=cand.url,
|
||||
fix=cand.fix,
|
||||
parent=cand.parent,
|
||||
family=cand.family,
|
||||
cwe=cwe,
|
||||
advisory=cand.advisory,
|
||||
advisory_text=osv_text(record),
|
||||
ecosystem=cand.ecosystem,
|
||||
license_spdx=cand.license_spdx,
|
||||
origin="osv-extend" if args.extend else "osv",
|
||||
)
|
||||
)
|
||||
excluded_repos.add(cand.repository)
|
||||
picked += 1
|
||||
counts["osv"] += 1
|
||||
log(f"osv pick {picked}/{target}: {cand.repository} {cand.advisory} {cand.ecosystem} {cand.family} {cand.license_spdx}")
|
||||
counts["github_api_calls"] = github.calls
|
||||
counts["osv_api_calls"] = osv_api.calls
|
||||
# the candidate list must not contain a held-out repository, whatever the origin
|
||||
assert not {row["repository"] for row in rows} & accepted
|
||||
stem = "sources_extend" if args.extend else "sources"
|
||||
(WORK / f"{stem}.json").write_text(json.dumps(rows, indent=1, ensure_ascii=False) + "\n")
|
||||
(WORK / f"{stem}_log.json").write_text(json.dumps(dict(sorted(counts.items())), indent=2) + "\n")
|
||||
log(
|
||||
json.dumps(
|
||||
{k: v for k, v in counts.items() if not k.startswith("osv:") or "check" in k or "pass" in k or "repositories" in k},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
log(f"sources.json: {len(rows)} advisories, {len({r['repository'] for r in rows})} repositories")
|
||||
return 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- clone
|
||||
|
||||
|
||||
def clone_one(repo: str, url: str, fix: str) -> tuple[Path | None, str, str | None]:
|
||||
"""Clone single-branch with history, make sure fix and fix^1 are present, return (dir, status, parent)."""
|
||||
target = HIST / repo.replace("/", "__")
|
||||
if not (target / ".git").exists():
|
||||
if target.exists():
|
||||
shutil.rmtree(target)
|
||||
done = subprocess.run(
|
||||
["git", "-c", "credential.helper=", "clone", "--quiet", "--single-branch", "--no-tags", "--no-checkout", url, str(target)],
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1800,
|
||||
env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
|
||||
)
|
||||
if done.returncode:
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
return None, "clone_failed", None
|
||||
# No working tree: everything downstream reads objects (git show / log / diff). The first 64 clones were
|
||||
# emptied the same way after the disk guard fired: `git sparse-checkout disable` restores a checkout.
|
||||
git(target, "sparse-checkout", "set", "--no-cone", "/__worktree_emptied_for_disk__", check=False)
|
||||
has_fix = git(target, "cat-file", "-e", f"{fix}^{{commit}}", check=False).returncode == 0
|
||||
if not has_fix:
|
||||
fetched = git(target, "fetch", "--quiet", "--depth", "2", "origin", fix, check=False)
|
||||
if fetched.returncode:
|
||||
fetched = git(target, "fetch", "--quiet", "origin", fix, check=False)
|
||||
if fetched.returncode or git(target, "cat-file", "-e", f"{fix}^{{commit}}", check=False).returncode:
|
||||
return target, "fix_unreachable", None
|
||||
parents = git(target, "rev-list", "--parents", "-n", "1", fix).stdout.split()[1:]
|
||||
if len(parents) != 1:
|
||||
return target, f"parents_{len(parents)}", None
|
||||
parent = parents[0]
|
||||
if git(target, "cat-file", "-e", f"{parent}^{{commit}}", check=False).returncode:
|
||||
fetched = git(target, "fetch", "--quiet", "--depth", "1", "origin", parent, check=False)
|
||||
if fetched.returncode or git(target, "cat-file", "-e", f"{parent}^{{commit}}", check=False).returncode:
|
||||
return target, "parent_unreachable", None
|
||||
# prove the parent's tree is readable: the catalog reads blobs at fix^1
|
||||
if git(target, "ls-tree", "-r", "--name-only", parent, check=False).returncode:
|
||||
return target, "parent_tree_unreadable", None
|
||||
return target, "ok", parent
|
||||
|
||||
|
||||
def cmd_clone(args) -> int:
|
||||
sources_path = Path(args.sources) if args.sources else WORK / "sources.json"
|
||||
rows = json.loads(sources_path.read_text())
|
||||
HIST.mkdir(parents=True, exist_ok=True)
|
||||
repos_path = WORK / "repos_hist.json"
|
||||
repos = json.loads(repos_path.read_text()) if repos_path.exists() else {}
|
||||
clone_log_path = WORK / "clone_log.json"
|
||||
clone_log = json.loads(clone_log_path.read_text()) if clone_log_path.exists() else {}
|
||||
by_repo: dict[str, list[dict]] = defaultdict(list)
|
||||
for row in rows:
|
||||
by_repo[row["repository"]].append(row)
|
||||
stopped = False
|
||||
for index, (repo, entries) in enumerate(sorted(by_repo.items()), 1):
|
||||
if repo in clone_log and clone_log[repo].get("status") == "ok" and all(e.get("parent_from_git") for e in entries):
|
||||
continue
|
||||
free = free_bytes(HIST)
|
||||
if free < MIN_FREE_BYTES:
|
||||
log(f"STOP: free space {free / 1024**3:.1f} GB under 6 GB before cloning {repo}")
|
||||
stopped = True
|
||||
break
|
||||
started = time.monotonic()
|
||||
url = entries[0]["url"]
|
||||
status, parent_seen = "ok", {}
|
||||
target = None
|
||||
for entry in entries:
|
||||
target, status, parent = clone_one(repo, url, entry["fix"])
|
||||
parent_seen[entry["fix"]] = parent
|
||||
if status != "ok":
|
||||
break
|
||||
size = sum(p.stat().st_size for p in target.rglob("*") if p.is_file()) if target and target.exists() else 0
|
||||
commits = (
|
||||
git(target, "rev-list", "--count", "--first-parent", "HEAD", check=False).stdout.strip() if target and status == "ok" else ""
|
||||
)
|
||||
clone_log[repo] = {
|
||||
"status": status,
|
||||
"bytes": size,
|
||||
"seconds": round(time.monotonic() - started, 1),
|
||||
"first_parent_commits": int(commits) if commits else None,
|
||||
"parents": parent_seen,
|
||||
}
|
||||
if status == "ok":
|
||||
repos[repo] = str(target)
|
||||
for entry in entries:
|
||||
entry["parent_from_git"] = parent_seen[entry["fix"]]
|
||||
else:
|
||||
if target and target.exists():
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
repos.pop(repo, None)
|
||||
log(
|
||||
f"[{index}/{len(by_repo)}] {repo}: {status} {size / 1024**2:.1f} MB {clone_log[repo]['seconds']}s free={free_bytes(HIST) / 1024**3:.1f}GB" # noqa: E501
|
||||
)
|
||||
repos_path.write_text(json.dumps(dict(sorted(repos.items())), indent=1) + "\n")
|
||||
clone_log_path.write_text(json.dumps(dict(sorted(clone_log.items())), indent=1) + "\n")
|
||||
# rewrite sources: parent = fix^1 from git; move uncloned entries aside
|
||||
kept, dropped = [], []
|
||||
for row in rows:
|
||||
entry = clone_log.get(row["repository"], {})
|
||||
parent = row.get("parent_from_git") or entry.get("parents", {}).get(row["fix"])
|
||||
if entry.get("status") == "ok" and parent:
|
||||
if row["parent"] != parent:
|
||||
row["source_parent"] = row["parent"]
|
||||
row["parent"] = parent
|
||||
row.pop("parent_from_git", None)
|
||||
kept.append(row)
|
||||
else:
|
||||
dropped.append({**row, "clone_status": entry.get("status", "not_cloned")})
|
||||
sources_path.write_text(json.dumps(kept, indent=1, ensure_ascii=False) + "\n")
|
||||
(sources_path.with_name(sources_path.stem + "_dropped.json")).write_text(json.dumps(dropped, indent=1, ensure_ascii=False) + "\n")
|
||||
total = sum(e["bytes"] for e in clone_log.values())
|
||||
log(
|
||||
f"cloned {sum(e['status'] == 'ok' for e in clone_log.values())}/{len(by_repo)} repositories, {total / 1024**3:.2f} GB, kept {len(kept)} sources, dropped {len(dropped)}, stopped={stopped}" # noqa: E501
|
||||
)
|
||||
return 1 if stopped else 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- filter
|
||||
|
||||
|
||||
def site_rejection(path: str) -> str | None:
|
||||
if MINIFIED_PATH.search(path):
|
||||
return "minified"
|
||||
if VENDORED_PATH.search(path):
|
||||
return "vendored"
|
||||
if TEST_PATH.search(path):
|
||||
return "test"
|
||||
return None
|
||||
|
||||
|
||||
def cmd_filter(args) -> int:
|
||||
from benchmarks.valid.catalog import write_catalog
|
||||
|
||||
cases = tomllib.loads(Path(args.catalog).read_text())["case"]
|
||||
kept, reasons = [], Counter()
|
||||
for case in cases:
|
||||
reason = site_rejection(case["path"])
|
||||
if reason:
|
||||
reasons[reason] += 1
|
||||
continue
|
||||
kept.append(case)
|
||||
write_catalog(Path(args.out), kept)
|
||||
log(f"catalog: {len(cases)} sites -> {len(kept)} kept, dropped {dict(reasons)}")
|
||||
(WORK / "catalog_filter_log.json").write_text(
|
||||
json.dumps({"input": len(cases), "kept": len(kept), "dropped": dict(reasons)}, indent=2) + "\n"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- verify
|
||||
|
||||
|
||||
class CountingClient:
|
||||
def __init__(self, inner, budget: int):
|
||||
self.inner, self.budget, self.calls = inner, budget, 0
|
||||
|
||||
def complete(self, **kwargs):
|
||||
if self.calls >= self.budget:
|
||||
raise BudgetExhausted()
|
||||
self.calls += 1
|
||||
return self.inner.complete(**kwargs)
|
||||
|
||||
|
||||
class BudgetExhausted(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def cmd_verify(args) -> int:
|
||||
from benchmarks.unseen.eligibility import normalize
|
||||
from benchmarks.valid.catalog import local_git
|
||||
from benchmarks.valid.verify import DEFAULT_DETECTOR_MODEL, DeepSeekChatClient, verify_case, write_records
|
||||
from openultrasast.config import load_config
|
||||
from openultrasast.model.endpoint import resolve_chat_endpoint
|
||||
|
||||
if not os.environ.get("DEEPSEEK_API_KEY"):
|
||||
log("DEEPSEEK_API_KEY is not exported")
|
||||
return 2
|
||||
resolved = resolve_chat_endpoint(load_config(dotenv=False))
|
||||
if resolved is None or resolved[1].provider != "deepseek":
|
||||
log("DeepSeek provider is required")
|
||||
return 2
|
||||
client = resolved[0]
|
||||
if isinstance(client, DeepSeekChatClient):
|
||||
client = client.bounded(1200)
|
||||
counting = CountingClient(client, args.budget)
|
||||
repos = {normalize(k): local_git(Path(v)) for k, v in json.loads(Path(args.repos).read_text()).items()}
|
||||
cases = tomllib.loads(Path(args.catalog).read_text())["case"]
|
||||
out = Path(args.out)
|
||||
records = {row["id"]: row for row in json.loads(out.read_text())["cases"]} if out.exists() else {}
|
||||
log_path = WORK / "verify_log.json"
|
||||
vlog = json.loads(log_path.read_text()) if log_path.exists() else {"calls": 0, "errors": {}, "passes": 0}
|
||||
# round-robin across advisories so an advisory with many sites cannot eat the budget alone
|
||||
groups: dict[str, list[dict]] = defaultdict(list)
|
||||
for case in sorted(cases, key=lambda c: c["id"]):
|
||||
groups[case["advisory"]].append(case)
|
||||
order = []
|
||||
depth = max(len(v) for v in groups.values())
|
||||
for level in range(depth):
|
||||
for advisory in sorted(groups):
|
||||
if level < len(groups[advisory]):
|
||||
order.append(groups[advisory][level])
|
||||
errors: Counter = Counter(vlog.get("errors", {}))
|
||||
started = time.monotonic()
|
||||
done = 0
|
||||
for case in order:
|
||||
if case["id"] in records and not (args.retry_errors and records[case["id"]].get("error")):
|
||||
continue
|
||||
if counting.calls >= args.budget:
|
||||
log(f"budget of {args.budget} calls reached")
|
||||
break
|
||||
try:
|
||||
record = verify_case(repos[normalize(case["repo"])], case, client=counting)
|
||||
except BudgetExhausted:
|
||||
break
|
||||
except Exception as exc: # provider or parse failure: record the type only, never the payload
|
||||
record = {
|
||||
"id": case["id"],
|
||||
"verdict": "rejected",
|
||||
"error": type(exc).__name__,
|
||||
"model": DEFAULT_DETECTOR_MODEL,
|
||||
"human_spot_check": None,
|
||||
}
|
||||
errors[type(exc).__name__] += 1
|
||||
records[case["id"]] = record
|
||||
done += 1
|
||||
passed = record.get("verdict") == "pending_human"
|
||||
log(
|
||||
f"{case['id']} -> {record.get('verdict')}{' PASS' if passed else ''}{' error=' + record['error'] if record.get('error') else ''} calls={counting.calls}" # noqa: E501
|
||||
)
|
||||
if done % 5 == 0:
|
||||
write_records(out, list(records.values()))
|
||||
write_records(out, list(records.values()))
|
||||
vlog["calls"] = vlog.get("calls", 0) + counting.calls
|
||||
vlog["errors"] = dict(errors)
|
||||
vlog["passes"] = sum(r.get("verdict") == "pending_human" for r in records.values())
|
||||
vlog["records"] = len(records)
|
||||
vlog["catalog_sites"] = len(cases)
|
||||
vlog["seconds"] = vlog.get("seconds", 0) + round(time.monotonic() - started)
|
||||
vlog["model"] = DEFAULT_DETECTOR_MODEL
|
||||
vlog["budget"] = args.budget
|
||||
log_path.write_text(json.dumps(vlog, indent=2, sort_keys=True) + "\n")
|
||||
log(json.dumps(vlog, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- candidates
|
||||
|
||||
|
||||
def cmd_candidates(args) -> int:
|
||||
cases = {c["id"]: c for c in tomllib.loads(Path(args.catalog).read_text())["case"]}
|
||||
records = json.loads(Path(args.verification).read_text())["cases"]
|
||||
sources = {row["advisory"]: row for row in json.loads((HERE / "sources.json").read_text())}
|
||||
passing = [r for r in records if r.get("verdict") == "pending_human"]
|
||||
by_repo: dict[str, list[dict]] = defaultdict(list)
|
||||
for record in passing:
|
||||
by_repo[cases[record["id"]]["repo"]].append(record)
|
||||
languages = Counter(cases[r["id"]]["language"] for r in passing)
|
||||
changes = {(cases[r["id"]]["repo"], cases[r["id"]]["commit"]) for r in passing}
|
||||
lines = [
|
||||
"# Training-set candidates for the maintainer's spot-check",
|
||||
"",
|
||||
f"Generated {date.today().isoformat()} by `benchmarks/valid/train/sourcing.py candidates`. Every location below passed all three",
|
||||
"model checks (vulnerable before the fix, fix addresses it, reachable from an in-repo entry point) and waits for a",
|
||||
"human accept or reject. Nothing here is a positive until accepted. Accept or reject a location with",
|
||||
"",
|
||||
" .venv/bin/python -m benchmarks.valid.verify spot-check benchmarks/valid/train/verification.json <id> accept|reject --reviewer <you> --date YYYY-MM-DD", # noqa: E501
|
||||
"",
|
||||
f"Sites passing: {len(passing)} in {len(by_repo)} repositories, {len(changes)} positive changes (fix commits); languages: "
|
||||
+ ", ".join(f"{k} {v}" for k, v in sorted(languages.items())),
|
||||
"",
|
||||
"The verifier's entry point is the model's claim, not a checked fact; it is the reason the site passed the reachability check.",
|
||||
"",
|
||||
]
|
||||
for repo in sorted(by_repo):
|
||||
rows = sorted(by_repo[repo], key=lambda r: r["id"])
|
||||
first = cases[rows[0]["id"]]
|
||||
src = sources.get(first["advisory"], {})
|
||||
lines += [
|
||||
f"## {repo}",
|
||||
"",
|
||||
f"- advisory: {first['advisory']} ({first['family']}, {first['cwe']}), origin `{src.get('origin', '?')}`, ecosystem {src.get('ecosystem', '?')}, licence {src.get('license_spdx', '?')}", # noqa: E501
|
||||
f"- fix `{first['commit']}`, parent (fix^1) `{first['parent']}`",
|
||||
f"- advisory title: {(first.get('advisory_text') or '').strip().splitlines()[0][:160] if first.get('advisory_text') else ''}",
|
||||
"",
|
||||
"| id | path::function (pre-fix lines) | language | entry point (verifier) | confidence |",
|
||||
"|---|---|---|---|---|",
|
||||
]
|
||||
for record in rows:
|
||||
case = cases[record["id"]]
|
||||
span = case["function_lines"][0]
|
||||
entry = str(record.get("entry_point", "")).replace("|", "\\|").replace("\n", " ")[:200]
|
||||
lines.append(
|
||||
f"| `{record['id']}` | `{case['path']}::{case['function']}` ({span[0]}-{span[1]}) | {case['language']} | {entry} | {record.get('confidence')} |" # noqa: E501
|
||||
)
|
||||
lines.append("")
|
||||
Path(args.out).write_text("\n".join(lines) + "\n")
|
||||
log(f"CANDIDATES.md: {len(passing)} sites, {len(by_repo)} repos, {len(changes)} changes, languages {dict(languages)}")
|
||||
return 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- overlap
|
||||
|
||||
GITHUB = re.compile(r"github\.com/([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)", re.IGNORECASE)
|
||||
REPO_LINE = re.compile(r'^\s*(?:repo|fix_repo)\s*=\s*"([^"]+)"', re.MULTILINE)
|
||||
|
||||
|
||||
def named_elsewhere() -> dict[str, set[str]]:
|
||||
"""Every repository the development tree already names, by where it is named. Mirrors the repository guards in
|
||||
tests/ (test_advisory_fix_pairs, test_independent_population): github.com/<name> anywhere under benchmarks/, src/,
|
||||
tests/, plane/ and .kiro/, plus `repo = "..."` lines, plus the strict used set. Vendored trees are skipped as the
|
||||
pair guard skips them; this directory is skipped because it is what is being checked."""
|
||||
from openultrasast.learn.labels import repo_name
|
||||
|
||||
found: dict[str, set[str]] = defaultdict(set)
|
||||
for top in ("benchmarks", "src", "tests", "plane", ".kiro"):
|
||||
for path in (ROOT / top).rglob("*"):
|
||||
if "node_modules" in path.parts or "vendor" in path.parts or path.is_relative_to(HERE):
|
||||
continue
|
||||
if not path.is_file() or path.suffix in {".pyc", ".bin", ".gz"}:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
label = "benchmarks/" + path.relative_to(ROOT / "benchmarks").parts[0] if top == "benchmarks" else top
|
||||
for match in GITHUB.findall(text):
|
||||
found[repo_name(match)].add(label)
|
||||
for match in REPO_LINE.findall(text):
|
||||
with contextlib.suppress(Exception): # an unparseable value is not a repository name
|
||||
found[repo_name(match)].add(label)
|
||||
for name in json.loads((WORK / "used_strict.json").read_text()):
|
||||
found[name].add("used_strict")
|
||||
return found
|
||||
|
||||
|
||||
def cmd_overlap(args) -> int:
|
||||
"""Drop every candidate whose repository the tree already names; write the surviving sources to the tree."""
|
||||
from openultrasast.learn.labels import repo_name
|
||||
|
||||
elsewhere = named_elsewhere()
|
||||
rows = [row for name in args.batches for row in json.loads((WORK / name).read_text())]
|
||||
kept, dropped = [], []
|
||||
for row in rows:
|
||||
where = sorted(elsewhere.get(repo_name(row["repository"]), ()))
|
||||
if where:
|
||||
dropped.append({"repository": row["repository"], "advisory": row["advisory"], "origin": row["origin"], "named_in": where})
|
||||
else:
|
||||
kept.append(row)
|
||||
assert not {repo_name(r["repository"]) for r in kept} & pilot_accepted()
|
||||
(WORK / "overlap_dropped.json").write_text(json.dumps(dropped, indent=1) + "\n")
|
||||
(WORK / "sources.json").write_text(json.dumps(kept, indent=1, ensure_ascii=False) + "\n")
|
||||
(HERE / "sources.json").write_text(json.dumps(kept, indent=1, ensure_ascii=False) + "\n")
|
||||
log(
|
||||
f"overlap: {len(rows)} sources, {len(dropped)} dropped (named elsewhere), {len(kept)} kept; by origin dropped {dict(Counter(d['origin'] for d in dropped))}" # noqa: E501
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_merge(args) -> int:
|
||||
"""Tree catalog.toml and verification.json: the batch files restricted to the surviving sources."""
|
||||
from benchmarks.valid.catalog import write_catalog
|
||||
from benchmarks.valid.verify import write_records
|
||||
|
||||
advisories = {row["advisory"] for row in json.loads((HERE / "sources.json").read_text())}
|
||||
cases = [c for name in args.catalogs for c in tomllib.loads((WORK / name).read_text()).get("case", []) if c["advisory"] in advisories]
|
||||
# A site whose code or advisory text links a repository the tree already names (a pair-corpus or reserved
|
||||
# repository in a code comment, say) would put that name back into benchmarks/: drop the site, record why.
|
||||
from openultrasast.learn.labels import repo_name
|
||||
|
||||
elsewhere = json.loads((WORK / "named_elsewhere.json").read_text())
|
||||
dropped = []
|
||||
for case in list(cases):
|
||||
names = {repo_name(m) for value in case.values() if isinstance(value, str) for m in GITHUB.findall(value)} - {case["repo"]}
|
||||
hits = sorted(n for n in names if n in elsewhere and not n.startswith("user-attachments/"))
|
||||
if hits:
|
||||
cases.remove(case)
|
||||
dropped.append({"id": case["id"], "repo": case["repo"], "names": hits})
|
||||
(WORK / "merge_dropped_sites.json").write_text(json.dumps(dropped, indent=1) + "\n")
|
||||
ids = {c["id"] for c in cases}
|
||||
records = [r for name in args.verifications for r in json.loads((WORK / name).read_text())["cases"] if r["id"] in ids]
|
||||
write_catalog(HERE / "catalog.toml", cases)
|
||||
write_records(HERE / "verification.json", records)
|
||||
log(f"merge: {len(cases)} sites, {len(records)} verification records, {len(dropped)} sites dropped for naming a used repository")
|
||||
return 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- precheck
|
||||
|
||||
|
||||
def cmd_precheck(args) -> int:
|
||||
from benchmarks.unseen import extract
|
||||
from benchmarks.unseen.eligibility import normalize
|
||||
from benchmarks.valid.catalog import local_git
|
||||
|
||||
cases = {c["id"]: c for c in tomllib.loads(Path(args.catalog).read_text())["case"]}
|
||||
records = json.loads(Path(args.verification).read_text())["cases"]
|
||||
repos = json.loads(Path(args.repos).read_text())
|
||||
passing = defaultdict(list)
|
||||
for record in records:
|
||||
if record.get("verdict") == "pending_human":
|
||||
passing[cases[record["id"]]["repo"]].append(cases[record["id"]])
|
||||
result = {}
|
||||
for repo in sorted(passing):
|
||||
git_ = local_git(Path(repos[repo]))
|
||||
all_cases = [c for c in cases.values() if normalize(c["repo"]) == repo]
|
||||
fixes = {c["commit"] for c in all_cases}
|
||||
functions = {(c["path"], c["function"]) for c in all_cases}
|
||||
head = git_.run("rev-parse", "HEAD").strip()
|
||||
started = time.monotonic()
|
||||
try:
|
||||
history = extract.ordinary_history(git_, head)
|
||||
except Exception as exc: # assemble.freeze calls the same function and would stop on this repository
|
||||
result[repo] = {"error": f"{type(exc).__name__}: {exc}", "seconds": round(time.monotonic() - started, 1)}
|
||||
log(f"{repo}: ordinary_history failed: {type(exc).__name__}: {exc}")
|
||||
(WORK / "precheck.json").write_text(json.dumps(result, indent=2, sort_keys=True) + "\n")
|
||||
continue
|
||||
available = sum(extract.ordinary_rejection(row, fixes, functions) is None for row in history)
|
||||
changes = len({(c["commit"], c["parent"]) for c in passing[repo]})
|
||||
result[repo] = {
|
||||
"first_parent_commits": len(history),
|
||||
"ordinary_available": available,
|
||||
"positive_changes": changes,
|
||||
"needed_at_ratio_20": 20 * changes,
|
||||
"enough": available >= 20 * changes,
|
||||
"seconds": round(time.monotonic() - started, 1),
|
||||
}
|
||||
(WORK / "precheck.json").write_text(json.dumps(result, indent=2, sort_keys=True) + "\n")
|
||||
log(
|
||||
f"{repo}: {available} ordinary of {len(history)} first-parent commits, needs {20 * changes}, enough={available >= 20 * changes}"
|
||||
)
|
||||
(WORK / "precheck.json").write_text(json.dumps(result, indent=2, sort_keys=True) + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
sub.add_parser("used")
|
||||
p = sub.add_parser("sources")
|
||||
p.add_argument("--osv-dir", required=True)
|
||||
p.add_argument("--target", type=int, default=75, help="OSV advisories to accept after the GitHub checks")
|
||||
p.add_argument("--seed", type=int, default=1)
|
||||
p.add_argument("--extend", action="store_true", help="second OSV batch into sources_extend.json, excluding sources.json")
|
||||
p = sub.add_parser("clone")
|
||||
p.add_argument("--sources", help="sources file to clone (default sources.json)")
|
||||
p = sub.add_parser("filter")
|
||||
p.add_argument("catalog")
|
||||
p.add_argument("--out", required=True)
|
||||
p = sub.add_parser("verify")
|
||||
p.add_argument("catalog")
|
||||
p.add_argument("--repos", required=True)
|
||||
p.add_argument("--out", required=True)
|
||||
p.add_argument("--budget", type=int, default=600)
|
||||
p.add_argument("--retry-errors", action="store_true")
|
||||
p = sub.add_parser("candidates")
|
||||
p.add_argument("verification")
|
||||
p.add_argument("catalog")
|
||||
p.add_argument("--out", required=True)
|
||||
p = sub.add_parser("overlap")
|
||||
p.add_argument("batches", nargs="+", help="source batch files in the work directory")
|
||||
p = sub.add_parser("merge")
|
||||
p.add_argument("--catalogs", nargs="+", required=True)
|
||||
p.add_argument("--verifications", nargs="+", required=True)
|
||||
p = sub.add_parser("precheck")
|
||||
p.add_argument("verification")
|
||||
p.add_argument("catalog")
|
||||
p.add_argument("--repos", required=True)
|
||||
args = parser.parse_args(argv)
|
||||
return {
|
||||
"used": cmd_used,
|
||||
"sources": cmd_sources,
|
||||
"clone": cmd_clone,
|
||||
"filter": cmd_filter,
|
||||
"verify": cmd_verify,
|
||||
"candidates": cmd_candidates,
|
||||
"precheck": cmd_precheck,
|
||||
"overlap": cmd_overlap,
|
||||
"merge": cmd_merge,
|
||||
}[args.command](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user