diff --git a/benchmarks/pairs/README.md b/benchmarks/pairs/README.md index 2b20eb6..3ca7897 100644 --- a/benchmarks/pairs/README.md +++ b/benchmarks/pairs/README.md @@ -40,6 +40,41 @@ Pairs in this directory **must not** join `LANGUAGE_MANIFESTS`. `vfc/training/manifest.jsonl` (vuln vs fixed, one snapshot per line). Grow with a blobless `git log --grep=CVE-202` mine plus `vfc/harvest.py`, then `vfc/generate_catalog.py` (maintainer, not CI). +- **vibe-py** — Real-Vuln-Benchmark functions (`benchmarks/pairs/vibe-py/`): + Python web repos pinned by commit, one vulnerable function per pair with a + false-positive trap from the same repo as the fixed twin. Today only the 15 + licensed human-authored repos are vendored (`provenance = "human"`); the 40 + LLM-generated repos carry no license file and are excluded until one is + stated upstream. Educational/CTF apps, not production. Honesty dashboard. +- **vfc-js** — server-side JavaScript function pairs harvested from the upstream + fix commits that SecBench.js records (`benchmarks/pairs/vfc-js/`). Command + injection, path traversal, and code injection first; prototype pollution and + ReDoS carry their mechanism and may be `known_limit`. No SecBench.js file is + vendored. Honesty dashboard. +- **agent-vfc** — security-fix commits carrying an AI agent co-author trailer or + generation marker, in JavaScript, TypeScript, or Python + (`benchmarks/pairs/agent-vfc/`). `provenance = "agent"`. Rows enter + `catalog.toml` only after a human sets `reviewer` in `recipes.toml`; until + then they sit in `catalog-candidates.toml`, which is never loaded. Honesty dashboard. + +## Labels, profiles, and mechanisms + +Every `[[pair.expected]]` row names a `mechanism` from the closed vocabulary in +`benchmarks/pairs/mechanisms.toml`; every pair carries `provenance` +(`human|agent|mixed|synthetic`), a declared `split` (`train|holdout`), and an +optional `known_limit` reason. The scorer counts overlay `coverage` records with +a source as detections and as leaks, matches by function when a label names one, +never matches a CWE-only row (`weak_label`), and reports `per_profile`, +`per_mechanism`, `achievable` (known-limit pairs excluded), and `loss` +(parse-failed files, unadjudicated proposals). `ousast pairs --profile agent +--split holdout --json` filters; `--hunter` adds the LLM hunter as a third scorer +when a hunter model is configured. The improve loop rejects a change that +regresses any profile on the holdout split. + +Harvest for every slice goes through `benchmarks/pairs/harvest.py --slice ` +(modes `name`, `line_range`, `hunk`, `enclosing`; comment and string mentions +never anchor) and `benchmarks/pairs/catalog_gen.py --slice ` regenerates +the catalog from `recipes.toml`. Maintainer only; CI never fetches. ## Commands diff --git a/benchmarks/pairs/agent-vfc/README.md b/benchmarks/pairs/agent-vfc/README.md new file mode 100644 index 0000000..4487749 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/README.md @@ -0,0 +1,18 @@ +# agent-vfc slice + +Security-fix commits carrying an AI coding agent co-author trailer or generation marker +(Claude Code, Codex, Cursor, Copilot, Devin), in JavaScript, TypeScript, or Python. +`provenance = "agent"`. Only repositories with a stated license are vendored. + +## Review checklist (Req 5.3) + +A recipe with `reviewer = "pending"` is a candidate, not a label. Before setting `reviewer`: + +1. The parent excerpt really contains the vulnerability the title claims (not a refactor). +2. One mechanism from `benchmarks/pairs/mechanisms.toml` explains it; adjust `mechanism` and `cwe`. +3. The fixed excerpt removes it and the change is inside the vendored function. +4. No secret, token, or personal data is in either excerpt (run the redaction check). +5. The license header is present and matches the repository license. + +Rebuild: `python benchmarks/pairs/agent-vfc/build_recipes.py --search /tmp/agents.jsonl` then +`--from /tmp/agents.jsonl`, `harvest.py --slice agent-vfc --all --fetch`, `catalog_gen.py --slice agent-vfc`. diff --git a/benchmarks/pairs/agent-vfc/REVIEW.md b/benchmarks/pairs/agent-vfc/REVIEW.md new file mode 100644 index 0000000..f72861e --- /dev/null +++ b/benchmarks/pairs/agent-vfc/REVIEW.md @@ -0,0 +1,45 @@ +# agent-vfc candidate review sheet + +Set `reviewer = ""` in `recipes.toml` for each row you confirm (and fix `mechanism`/`cwe` if the title-based guess is wrong), then run `python benchmarks/pairs/catalog_gen.py --slice agent-vfc`. Rows without a reviewer stay in `catalog-candidates.toml` and are never scored. + +| # | name | mechanism (guess) | cwe | language | commit | title | +|---|------|-------------------|-----|----------|--------|-------| +| 1 | `academic-based-qoder-app-a79ddb` | permissive_default | CWE-16 | python | [a79ddb90](https://github.com/1173591564/academic-based-qoder/commit/a79ddb90def54e91f7507cbf6b1548b224d642aa) | fix(hub): allow DSH token validation CORS | +| 2 | `anastasios-pw-session-14ec1a` | source_reaches_sink | CWE-918 | typescript | [14ec1ac5](https://github.com/Anas329796/Anastasios/commit/14ec1ac50f9e4e255d875236d30129c6207401d4) | fix(browser): harden SSRF redirect guard against non-navigation document hops [A | +| 3 | `arsnova-eu-httpcors-9bcba4` | permissive_default | CWE-16 | typescript | [9bcba4f4](https://github.com/kqc-real/arsnova.eu/commit/9bcba4f442aecc6a5e16ef2dc6115177cfc2b6df) | fix(security): Produktions-CORS auf Same-Origin begrenzen (#143) | +| 4 | `astroclaw-pw-session-1553a8` | source_reaches_sink | CWE-918 | typescript | [1553a8ad](https://github.com/P1R4N351/astroclaw/commit/1553a8adf123913cf7f1f1c75be4c4e5cd6c796b) | fix(browser): harden SSRF redirect guard against non-navigation document hops [A | +| 5 | `capgo-app-apptabs-7d73fd` | permissive_default | CWE-16 | typescript | [7d73fd21](https://github.com/Cap-go/capgo.app/commit/7d73fd21a59eaa3a4e20c6dc0d280e73e47435b7) | fix(notifications): harden API CORS and mark console tab as beta (#2623) | +| 6 | `capgo-app-hono-4dadd2` | permissive_default | CWE-16 | typescript | [4dadd258](https://github.com/Cap-go/capgo.app/commit/4dadd2580d02213d227a331eee0232aa59859b59) | fix(api): CORS for Capacitor localhost WebView (#3105) | +| 7 | `dailyforge-server-ed1470` | permissive_default | CWE-16 | javascript | [ed147081](https://github.com/Annamdevula-Nissi-Grace/Dailyforge/commit/ed1470814a108bd725c8b27bd9d86cf95a979a96) | Fix: add localhost fallback for CORS origin (#172) | +| 8 | `dnf-plugin-anyrepo-config-26d19a` | path_join_user_input | CWE-22 | python | [26d19aeb](https://github.com/jfut/dnf-plugin-anyrepo/commit/26d19aeb8884f244e34ccfdc4f49f07055adc4d0) | fix(config): prevent path traversal in repository names | +| 9 | `edutrace-page-ee7ba2` | prototype_pollution | CWE-1321 | typescript | [ee7ba25f](https://github.com/jardimdesoftware/edutrace/commit/ee7ba25fb23ea89cc1b67549cb5e32c8ead92739) | fix(front): impedir prototype pollution nos handlers de formulário aninhado | +| 10 | `etoj-index-605274` | permissive_default | CWE-16 | typescript | [6052743d](https://github.com/Eternity-Sky/_ETOJ/commit/6052743d477eeff62e32553c2790785d3f52c641) | Fix CORS: Add PATCH method to allowed methods | +| 11 | `etude-lexical-markdown-editorconfig-c61a8b` | prototype_pollution | CWE-1321 | typescript | [c61a8bf7](https://github.com/dayflower/etude-lexical-markdown/commit/c61a8bf78fe20d6ae0eab2e4b8b02004508525ed) | fix: guard deepMergeInto against prototype pollution | +| 12 | `etude-lexical-markdown-editorconfig-db4f54` | prototype_pollution | CWE-1321 | typescript | [db4f54b1](https://github.com/dayflower/etude-lexical-markdown/commit/db4f54b1ab80ad596de6425e84f90aa781c49999) | fix: make prototype pollution guard recognizable by CodeQL | +| 13 | `gitim-git-cors-proxy-e4c3a4` | permissive_default | CWE-16 | typescript | [e4c3a479](https://github.com/CiferaTeam/GitIM/commit/e4c3a4794dbe9fbcce679f7df1ecd2b09b40f9d6) | fix(frontend): use gitim cors proxy by default | +| 14 | `lensops-kubernetesmanagement-fe4910` | missing_auth_guard | CWE-862 | typescript | [fe49109a](https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84) | Fix missing auth header on namespaces API call | +| 15 | `market-cap-route-4f8ae9` | source_reaches_sink | CWE-601 | typescript | [4f8ae9be](https://github.com/navaneethbv/market-cap/commit/4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6) | Fix open redirects on the auth next param | +| 16 | `melodix-playerprovider-089931` | permissive_default | CWE-16 | typescript | [089931ef](https://github.com/alexshao3/melodix/commit/089931ef71ea1e5b7586520baf1a5498e2f05b13) | Merge pull request #17 from alexshao3/devin/1777216143-fix-audio-cors | +| 17 | `mercado-do-vale-news-catalogmetadataservice-9b5fcf` | permissive_default | CWE-16 | typescript | [9b5fcf2d](https://github.com/handielson/Mercado-do-vale-news/commit/9b5fcf2dae41e56f5f97952266f2d6ba59598598) | fix(catalog): avoid metadata cors preflight | +| 18 | `money-track-category-icons-4d4a5b` | prototype_pollution | CWE-1321 | typescript | [4d4a5b06](https://github.com/andrebubniak/money-track/commit/4d4a5b063278c8b6697c3faae133d7c39fcd01c0) | fix(category-icons): use Object.hasOwn to prevent prototype pollution in type gu | +| 19 | `nestjs-filter-normalizer-aa0e60` | prototype_pollution | CWE-1321 | typescript | [aa0e600d](https://github.com/DavideCarvalho/nestjs-filter/commit/aa0e600d05997ea3d77538ea1c2db427c439f2e8) | fix(core): add prototype pollution guard to normalizeInput | +| 20 | `new-erp-final-app-6e9644` | permissive_default | CWE-16 | typescript | [6e9644c8](https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59) | fix: allow patch requests through cors | +| 21 | `nimestream-route-d17f53` | source_reaches_sink | CWE-918 | javascript | [d17f535b](https://github.com/Irnhakim/nimestream/commit/d17f535bf3012041b056ff3aa991ff6cfa198e43) | fix: resolve command injection and SSRF/Open Redirect security issues in multiSc | +| 22 | `onlook-helper-57ce1c` | missing_auth_guard | CWE-862 | typescript | [57ce1ccf](https://github.com/zhcndoc/onlook/commit/57ce1ccfacc7e8484cae59013a2b1e34518560e5) | Fix CVE-2025-63783: Add authorization checks to project mutation APIs (#3062) | +| 23 | `openniw-jobs-0fc947` | identity_from_request_body | CWE-639 | python | [0fc94733](https://github.com/HHHHHejia/openniw/commit/0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10) | Fix IDOR on job endpoint: case-bound jobs require case ownership | +| 24 | `openniw-jobs-b74de3` | identity_from_request_body | CWE-639 | python | [b74de31b](https://github.com/ziyuzuo/openniw/commit/b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68) | Fix IDOR on job endpoint: case-bound jobs require case ownership | +| 25 | `overleaf-with-claude-exportscontroller-3980b9` | identity_from_request_body | CWE-639 | javascript | [3980b9e5](https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431) | Fix IDOR in exports by adding token verification (Issue #31637) (#32883) | +| 26 | `popmark-editorconfig-e5ac40` | prototype_pollution | CWE-1321 | typescript | [e5ac407c](https://github.com/dayflower/popmark/commit/e5ac407cef37c37178d440541ede301c838640fb) | fix: guard deepMergeInto against prototype pollution | +| 27 | `rag-pipeline-utils-config-handler-76e5f0` | prototype_pollution | CWE-1321 | javascript | [76e5f0a9](https://github.com/DevilsDev/rag-pipeline-utils/commit/76e5f0a94d6803d0e463d26a61cedb7da90857a7) | fix(security): prevent prototype pollution in config-handler | +| 28 | `registry-proxy-worker-426847` | permissive_default | CWE-16 | javascript | [42684789](https://github.com/agents-repo/registry-proxy/commit/426847899eb2ee94ae4e5a3aacc50423e36d2dc8) | fix: handle CORS OPTIONS preflight for catalog fetches (#10) | +| 29 | `rls-alembic-rls-d2b6af` | permissive_default | CWE-16 | python | [d2b6af47](https://github.com/DelfinaCare/rls/commit/d2b6af47347fb285b7a3820a7f2d5dded20907ec) | Guard against ENABLE ROW LEVEL SECURITY on tables with no policies (#143) | +| 30 | `supplieradvisor-mvp-route-387bcf` | identity_from_request_body | CWE-639 | typescript | [387bcf40](https://github.com/bigfivefoods/supplieradvisor-mvp/commit/387bcf40e984a2ee1832ab7beabf0284c5b627d8) | Brief 62: Fix IDOR on customer_claims PATCH/DELETE (#156) | +| 31 | `supplieradvisor-mvp-route-a69e8f` | identity_from_request_body | CWE-639 | typescript | [a69e8f27](https://github.com/bigfivefoods/supplieradvisor-mvp/commit/a69e8f27c0f4d402573d16f0612b51155bbc46e1) | Brief 60: Fix IDOR on opportunities GET/PATCH/DELETE (missing company membership | +| 32 | `tools-cli-tools-a96bc0` | source_reaches_sink | CWE-78 | python | [a96bc050](https://github.com/D-sorganization/Tools/commit/a96bc0502a3dcdac303e4f0152508886e5c35c0a) | 🛡️ Sentinel: [CRITICAL] Fix command injection in ShellTool (#3307) | +| 33 | `tools-wsgi-e18686` | path_join_user_input | CWE-22 | python | [e18686aa](https://github.com/D-sorganization/Tools/commit/e18686aaa174a8a4843c07b4727771c92e0f179e) | fix(security): validate mesh filenames against path traversal in _download_meshe | +| 34 | `trackly-cli-path-diagnostics-1c1aed` | path_join_user_input | CWE-22 | javascript | [1c1aed89](https://github.com/trackly-app/trackly-cli/commit/1c1aed89b07edb04051830c8f086e02b65e961c6) | fix(apply): report blocked path traversal | +| 35 | `upstreamdrift-local-server-c3a078` | path_join_user_input | CWE-22 | python | [c3a07870](https://github.com/D-sorganization/UpstreamDrift/commit/c3a0787080489b30e2ed0abfac03b51e45da184b) | fix(api): harden local server path traversal (#2805) (#2839) | +| 36 | `vantalk-firebase-16c3b0` | secret_in_client | CWE-798 | typescript | [16c3b028](https://github.com/NiSeullent/VanTalk/commit/16c3b02875b1f51c3cac1dc185e199978a35985c) | fix(security): load Firebase API key from env, remove hardcoded secret | +| 37 | `waggledance-swarm-ssrf-host-guard-9a4548` | source_reaches_sink | CWE-918 | python | [9a454843](https://github.com/Ahkeratmehilaiset/waggledance-swarm/commit/9a454843aa35332ff2eb3106023eee0c4b5e444b) | fix(ssrf): reject bare-string allowed hosts (#1460) | +| 38 | `workers-research-utils-209444` | source_reaches_sink | CWE-89 | typescript | [209444eb](https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e) | Fix SQL injection in search by using parameterized queries (#41) | +| 39 | `zoonk-origin-bfbf67` | permissive_default | CWE-16 | typescript | [bfbf670c](https://github.com/zoonk/zoonk/commit/bfbf670ccb41d63c407d32c51bd26e626a174f31) | fix(api): allow staging cors from previews (#1559) | diff --git a/benchmarks/pairs/agent-vfc/anastasios-pw-session-14ec1a/fixed.ts b/benchmarks/pairs/agent-vfc/anastasios-pw-session-14ec1a/fixed.ts new file mode 100644 index 0000000..7714458 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/anastasios-pw-session-14ec1a/fixed.ts @@ -0,0 +1,38 @@ +// Provenance: Anas329796/Anastasios (fixed). +// repo: Anas329796/Anastasios +// commit: 14ec1ac50f9e4e255d875236d30129c6207401d4 +// parent: adb7b0d5d6b81c46d56451c709e7eac9d0dc93fb +// commit_url: https://github.com/Anas329796/Anastasios/commit/14ec1ac50f9e4e255d875236d30129c6207401d4 +// cve: +// license: MIT +// function: isTopLevelNavigationRequest +// relpath: extensions/browser/src/browser/pw-session.ts +// provenance: agent +// mechanism: source_reaches_sink + +function isTopLevelNavigationRequest(page: Page, request: Request): boolean { + let sameMainFrame = false; + try { + sameMainFrame = request.frame() === page.mainFrame(); + } catch { + // Frame resolution can fail during redirect/renderer churn; fail closed. + sameMainFrame = true; + } + if (!sameMainFrame) { + return false; + } + + try { + if (request.isNavigationRequest()) { + return true; + } + } catch { + // Ignore and fall back to resource-type check below. + } + + try { + return request.resourceType() === "document"; + } catch { + return false; + } +} diff --git a/benchmarks/pairs/agent-vfc/anastasios-pw-session-14ec1a/vuln.ts b/benchmarks/pairs/agent-vfc/anastasios-pw-session-14ec1a/vuln.ts new file mode 100644 index 0000000..a177d4e --- /dev/null +++ b/benchmarks/pairs/agent-vfc/anastasios-pw-session-14ec1a/vuln.ts @@ -0,0 +1,22 @@ +// Provenance: Anas329796/Anastasios (vuln). +// repo: Anas329796/Anastasios +// commit: adb7b0d5d6b81c46d56451c709e7eac9d0dc93fb +// parent: adb7b0d5d6b81c46d56451c709e7eac9d0dc93fb +// commit_url: https://github.com/Anas329796/Anastasios/commit/14ec1ac50f9e4e255d875236d30129c6207401d4 +// cve: +// license: MIT +// function: isTopLevelNavigationRequest +// relpath: extensions/browser/src/browser/pw-session.ts +// provenance: agent +// mechanism: source_reaches_sink + +function isTopLevelNavigationRequest(page: Page, request: Request): boolean { + if (!request.isNavigationRequest()) { + return false; + } + try { + return request.frame() === page.mainFrame(); + } catch { + return true; + } +} diff --git a/benchmarks/pairs/agent-vfc/astroclaw-pw-session-1553a8/fixed.ts b/benchmarks/pairs/agent-vfc/astroclaw-pw-session-1553a8/fixed.ts new file mode 100644 index 0000000..2ff1327 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/astroclaw-pw-session-1553a8/fixed.ts @@ -0,0 +1,38 @@ +// Provenance: P1R4N351/astroclaw (fixed). +// repo: P1R4N351/astroclaw +// commit: 1553a8adf123913cf7f1f1c75be4c4e5cd6c796b +// parent: d3eed2dc01328f21dd404477bb28e2740fa371a4 +// commit_url: https://github.com/P1R4N351/astroclaw/commit/1553a8adf123913cf7f1f1c75be4c4e5cd6c796b +// cve: +// license: MIT +// function: isTopLevelNavigationRequest +// relpath: extensions/browser/src/browser/pw-session.ts +// provenance: agent +// mechanism: source_reaches_sink + +function isTopLevelNavigationRequest(page: Page, request: Request): boolean { + let sameMainFrame = false; + try { + sameMainFrame = request.frame() === page.mainFrame(); + } catch { + // Frame resolution can fail during redirect/renderer churn; fail closed. + sameMainFrame = true; + } + if (!sameMainFrame) { + return false; + } + + try { + if (request.isNavigationRequest()) { + return true; + } + } catch { + // Ignore and fall back to resource-type check below. + } + + try { + return request.resourceType() === "document"; + } catch { + return false; + } +} diff --git a/benchmarks/pairs/agent-vfc/astroclaw-pw-session-1553a8/vuln.ts b/benchmarks/pairs/agent-vfc/astroclaw-pw-session-1553a8/vuln.ts new file mode 100644 index 0000000..20a7e46 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/astroclaw-pw-session-1553a8/vuln.ts @@ -0,0 +1,22 @@ +// Provenance: P1R4N351/astroclaw (vuln). +// repo: P1R4N351/astroclaw +// commit: d3eed2dc01328f21dd404477bb28e2740fa371a4 +// parent: d3eed2dc01328f21dd404477bb28e2740fa371a4 +// commit_url: https://github.com/P1R4N351/astroclaw/commit/1553a8adf123913cf7f1f1c75be4c4e5cd6c796b +// cve: +// license: MIT +// function: isTopLevelNavigationRequest +// relpath: extensions/browser/src/browser/pw-session.ts +// provenance: agent +// mechanism: source_reaches_sink + +function isTopLevelNavigationRequest(page: Page, request: Request): boolean { + if (!request.isNavigationRequest()) { + return false; + } + try { + return request.frame() === page.mainFrame(); + } catch { + return true; + } +} diff --git a/benchmarks/pairs/agent-vfc/build_recipes.py b/benchmarks/pairs/agent-vfc/build_recipes.py new file mode 100644 index 0000000..30bfb00 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/build_recipes.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +"""Build agent-vfc candidate recipes from agent-authored security-fix commits (maintainer, network via gh). + +Candidates come from GitHub commit search on agent co-author trailers or generation +markers plus security-fix wording (see --search). Each candidate is kept only when the +repository states a license, the fix touches one to three JavaScript, TypeScript, or +Python files, and the largest changed file has at most --max-lines changed lines. +Every recipe starts with ``reviewer = "pending"``; a human must set the reviewer and +confirm the mechanism before the pair is trusted (Req 5.3). + + python benchmarks/pairs/agent-vfc/build_recipes.py --search /tmp/agent_candidates.jsonl + python benchmarks/pairs/agent-vfc/build_recipes.py --from /tmp/agent_candidates.jsonl + python benchmarks/pairs/harvest.py --slice agent-vfc --all --fetch + python benchmarks/pairs/catalog_gen.py --slice agent-vfc +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +TRAILERS = ( + '"Co-Authored-By: Claude"', + '"Generated with Claude Code"', + '"Co-authored-by: Cursor"', + '"Co-authored-by: Codex"', + '"Co-authored-by: Copilot"', + '"Co-authored-by: devin"', +) +KEYWORDS = ( + '"fix IDOR"', + '"SQL injection"', + '"path traversal"', + "SSRF", + '"hardcoded secret"', + '"CORS"', + '"row level security"', + '"authorization check"', + '"command injection"', + '"prototype pollution"', + '"open redirect"', + '"missing authentication"', +) +MECHANISM_BY_TITLE = ( + ( + re.compile(r"\bidor\b|ownership|owner.?scope|user_id from|tenant", re.I), + "identity_from_request_body", + "CWE-639", + "insecure direct object reference", + ), + ( + re.compile(r"missing auth|unauthenticated|auth(?:orization|entication)? (?:check|guard|middleware)|require.?auth", re.I), + "missing_auth_guard", + "CWE-862", + "missing authorization", + ), + (re.compile(r"sql injection|sqli", re.I), "source_reaches_sink", "CWE-89", "sql injection"), + (re.compile(r"path traversal|zip slip|directory traversal", re.I), "path_join_user_input", "CWE-22", "path traversal"), + (re.compile(r"\bssrf\b", re.I), "source_reaches_sink", "CWE-918", "server-side request forgery"), + (re.compile(r"command injection|shell injection", re.I), "source_reaches_sink", "CWE-78", "command injection"), + (re.compile(r"prototype pollution", re.I), "prototype_pollution", "CWE-1321", "prototype pollution"), + (re.compile(r"open redirect", re.I), "source_reaches_sink", "CWE-601", "open redirect"), + ( + re.compile(r"hardcoded|hard-coded|leaked (?:key|secret|token)|service.?role", re.I), + "secret_in_client", + "CWE-798", + "hardcoded credentials", + ), + (re.compile(r"\bcors\b|row level security|\brls\b|debug", re.I), "permissive_default", "CWE-16", "permissive default"), +) +ACCEPTED_LICENSES = { + "MIT", + "Apache-2.0", + "BSD-3-Clause", + "BSD-2-Clause", + "ISC", + "MPL-2.0", + "GPL-3.0", + "GPL-2.0", + "AGPL-3.0", + "Unlicense", + "0BSD", +} +_CODE = re.compile(r"\.(py|js|mjs|cjs|ts|tsx)$") +_SKIP = re.compile(r"(test|spec|\.d\.ts|migration|dist/|build/|min\.js|\.config\.|stories)", re.I) +_NOISE = re.compile(r"\b(css|icon|i18n|layout|docs?|tracker|sess[aã]o|chore|bump|migration|workflow)\b", re.I) + + +def _gh(args: list[str]) -> object: + return json.loads(subprocess.check_output(["gh", "api", *args], stderr=subprocess.DEVNULL)) + + +def search(out: Path) -> int: + seen: set[tuple[str, str]] = set() + with out.open("w") as handle: + for trailer in TRAILERS: + for keyword in KEYWORDS: + time.sleep(6) # commit search has a strict secondary rate limit + try: + result = _gh(["-X", "GET", "search/commits", "-f", f"q={trailer} {keyword} fix", "-f", "per_page=30"]) + except Exception: # noqa: BLE001 — rate limited; keep going + continue + for item in result.get("items", []) if isinstance(result, dict) else []: + key = (item["repository"]["full_name"], item["sha"]) + if key in seen: + continue + seen.add(key) + handle.write( + json.dumps( + { + "repo": key[0], + "sha": key[1], + "title": item["commit"]["message"].split("\n")[0][:160], + "date": item["commit"]["author"]["date"], + } + ) + + "\n" + ) + return len(seen) + + +def classify(title: str) -> tuple[str, str, str] | None: + for pattern, mechanism, cwe, cls in MECHANISM_BY_TITLE: + if pattern.search(title): + return mechanism, cwe, cls + return None + + +def build(source: Path, *, limit: int, max_lines: int) -> tuple[list[dict[str, object]], dict[str, int]]: + recipes: list[dict[str, object]] = [] + skipped = {"noise_title": 0, "unclassified": 0, "unlicensed": 0, "no_code_file": 0, "too_big": 0, "api": 0, "duplicate_repo": 0} + per_repo: dict[str, int] = {} + seen_commits: set[str] = set() # the same fix commit shows up in every fork + rows = [json.loads(line) for line in source.read_text().splitlines() if line.strip()] + for row in sorted(rows, key=lambda r: (r["repo"], r["date"])): + if len(recipes) >= limit: + break + title = str(row["title"]) + if _NOISE.search(title): + skipped["noise_title"] += 1 + continue + classified = classify(title) + if classified is None: + skipped["unclassified"] += 1 + continue + if per_repo.get(row["repo"], 0) >= 2 or row["sha"] in seen_commits: + skipped["duplicate_repo"] += 1 + continue + seen_commits.add(row["sha"]) + time.sleep(0.8) + try: + info = _gh([f"repos/{row['repo']}"]) + commit = _gh([f"repos/{row['repo']}/commits/{row['sha']}"]) + except Exception: # noqa: BLE001 + skipped["api"] += 1 + continue + assert isinstance(info, dict) and isinstance(commit, dict) + license_id = (info.get("license") or {}).get("spdx_id") or "" + if license_id not in ACCEPTED_LICENSES: + skipped["unlicensed"] += 1 + continue + files = [f for f in commit.get("files", []) if _CODE.search(f["filename"]) and not _SKIP.search(f["filename"]) and f.get("patch")] + if not files or len(files) > 3: + skipped["no_code_file"] += 1 + continue + target = sorted(files, key=lambda f: -(f.get("additions", 0) + f.get("deletions", 0)))[0] + if target.get("additions", 0) + target.get("deletions", 0) > max_lines: + skipped["too_big"] += 1 + continue + parents = commit.get("parents") or [] + if not parents: + skipped["api"] += 1 + continue + mechanism, cwe, cls = classified + message = commit["commit"]["message"] + trailer = next((line.strip() for line in message.splitlines() if re.match(r"(?i)co-authored-by:|generated with", line.strip())), "") + suffix = Path(target["filename"]).suffix + language = {".py": "python", ".ts": "typescript", ".tsx": "typescript"}.get(suffix, "javascript") + base = re.sub(r"[^a-z0-9]+", "-", f"{row['repo'].split('/')[-1]}-{Path(target['filename']).stem}".lower()).strip("-")[:56] + name = f"{base}-{row['sha'][:6]}" + per_repo[row["repo"]] = per_repo.get(row["repo"], 0) + 1 + recipes.append( + { + "name": name, + "host": "github", + "repo": row["repo"], + "parent": parents[0]["sha"], + "commit": commit["sha"], + "path": target["filename"], + "mode": "hunk", + "language": language, + "relpath": target["filename"], + "license": license_id, + "cve": "", + "year": int(str(commit["commit"]["author"]["date"])[:4]), + "cwe": cwe, + "class": cls, + "mechanism": mechanism, + "provenance": "agent", + "origin": "github-agent-trailer", + "project": row["repo"].split("/")[-1], + "trailer": trailer[:120], + "reviewer": "pending", + "evidence": title[:200], + "commit_url": f"https://github.com/{row['repo']}/commit/{commit['sha']}", + } + ) + for index, recipe in enumerate(sorted(recipes, key=lambda r: str(r["name"]))): + recipe["split"] = "holdout" if index % 2 else "train" + return recipes, skipped + + +def to_toml(recipes: list[dict[str, object]]) -> str: + lines = [ + "# agent-vfc candidate recipes from agent-trailer security-fix commits (build_recipes.py).", + '# reviewer = "pending" means a human has not yet confirmed the vulnerability and mechanism.', + "", + ] + for recipe in sorted(recipes, key=lambda r: str(r["name"])): + lines.append("[[recipe]]") + for key, value in recipe.items(): + if isinstance(value, int) and not isinstance(value, bool): + lines.append(f"{key} = {value}") + else: + lines.append(f'{key} = "{str(value).replace(chr(92), chr(92) * 2).replace(chr(34), chr(92) + chr(34))}"') + lines.append("") + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--search", type=Path, help="run the GitHub commit search and write candidates to this jsonl") + parser.add_argument("--from", dest="source", type=Path, help="candidate jsonl to resolve into recipes") + parser.add_argument("--limit", type=int, default=40) + parser.add_argument("--max-lines", type=int, default=80) + parser.add_argument("--out", type=Path, default=ROOT / "recipes.toml") + args = parser.parse_args(argv) + if args.search: + print(f"searched {search(args.search)} candidates -> {args.search}") + if args.source: + recipes, skipped = build(args.source, limit=args.limit, max_lines=args.max_lines) + args.out.write_text(to_toml(recipes), encoding="utf-8") + print(f"wrote {args.out} ({len(recipes)} recipes); skipped {skipped}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/pairs/agent-vfc/capgo-app-hono-4dadd2/fixed.ts b/benchmarks/pairs/agent-vfc/capgo-app-hono-4dadd2/fixed.ts new file mode 100644 index 0000000..7445b2a --- /dev/null +++ b/benchmarks/pairs/agent-vfc/capgo-app-hono-4dadd2/fixed.ts @@ -0,0 +1,91 @@ +// Provenance: Cap-go/capgo.app (fixed). +// repo: Cap-go/capgo.app +// commit: 4dadd2580d02213d227a331eee0232aa59859b59 +// parent: 8c0ac7e233946abd8dc26581b871e9fcd0928899 +// commit_url: https://github.com/Cap-go/capgo.app/commit/4dadd2580d02213d227a331eee0232aa59859b59 +// cve: +// license: AGPL-3.0 +// function: next +// relpath: supabase/functions/_backend/utils/hono.ts +// provenance: agent +// mechanism: permissive_default + +export function createHono(functionName: string, _version: string) { + let appGlobal + if (getRuntimeKey() === 'deno') { + appGlobal = new Hono().basePath(`/${functionName}`) + } + else { + appGlobal = new Hono() + } + + // Plugin hot paths (/updates|/stats|/channel_self and the CF plugin worker). + // HAR/CPU inspect showed middleware + logging dominating non-DB request cost. + const pluginHotPath = functionName === 'plugin' + || functionName === 'updates' + || functionName === 'stats' + || functionName === 'channel_self' + + appGlobal.use('*', (c, next): Promise => { + // ADD HEADER TO IDENTIFY WORKER SOURCE + const name = `${getEnv(c, 'ENV_NAME') || functionName}-${CapgoVersion}` + c.header('X-Worker-Source', name) + const hostname = new URL(c.req.url).hostname + if (!isPreviewHost(hostname)) + c.header('Content-Security-Policy', API_CONTENT_SECURITY_POLICY) + return next() + }) + + // Echo ACAO after the handler so Capacitor WebView origins work on routes + // that forgot useCors (GET /ok). Do not mount hono cors globally: it + // short-circuits OPTIONS and strips TUS discovery headers. + if (!pluginHotPath) { + appGlobal.use('*', async (c, next) => { + await next() + const origin = c.req.header('origin') + if (!origin) + return + const allowed = getAllowedCorsOrigin(origin, c) + if (allowed) { + c.header('Access-Control-Allow-Origin', allowed) + c.header('Vary', 'Origin', { append: true }) + } + }) + } + + // Skip hono's access logger on plugin hot paths — it serializes every request + // on millions of device calls/day. + if (!pluginHotPath) + appGlobal.use('*', logger()) + // Use platform-specific request IDs, fallback to generated UUID. + // Do not cloudlog inside the generator: it runs on every request (incl. cf-ray). + appGlobal.use('*', requestId({ + generator: (c) => { + // Cloudflare provides the Ray ID in the cf-ray header + // Check this first as it's our primary deployment target + const cfRay = c.req.header('cf-ray') + if (cfRay) + return cfRay + // Supabase Edge Functions provide SB_EXECUTION_ID + const sbExecutionId = getEnv(c, 'SB_EXECUTION_ID') + if (sbExecutionId) + return sbExecutionId + // Fallback to crypto.randomUUID() if not on any known platform + return crypto.randomUUID() + }, + })) + + appGlobal.post('/ok', (c) => { + return c.json(BRES) + }) + appGlobal.post('/ko', (c) => { + const defaultResponse: SimpleErrorResponse = { + error: 'unknown_error', + message: 'KO', + moreInfo: {}, + } + return c.json(defaultResponse, 500) + }) + + return appGlobal +} diff --git a/benchmarks/pairs/agent-vfc/capgo-app-hono-4dadd2/vuln.ts b/benchmarks/pairs/agent-vfc/capgo-app-hono-4dadd2/vuln.ts new file mode 100644 index 0000000..bc40328 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/capgo-app-hono-4dadd2/vuln.ts @@ -0,0 +1,74 @@ +// Provenance: Cap-go/capgo.app (vuln). +// repo: Cap-go/capgo.app +// commit: 8c0ac7e233946abd8dc26581b871e9fcd0928899 +// parent: 8c0ac7e233946abd8dc26581b871e9fcd0928899 +// commit_url: https://github.com/Cap-go/capgo.app/commit/4dadd2580d02213d227a331eee0232aa59859b59 +// cve: +// license: AGPL-3.0 +// function: next +// relpath: supabase/functions/_backend/utils/hono.ts +// provenance: agent +// mechanism: permissive_default + +export function createHono(functionName: string, _version: string) { + let appGlobal + if (getRuntimeKey() === 'deno') { + appGlobal = new Hono().basePath(`/${functionName}`) + } + else { + appGlobal = new Hono() + } + + // Plugin hot paths (/updates|/stats|/channel_self and the CF plugin worker). + // HAR/CPU inspect showed middleware + logging dominating non-DB request cost. + const pluginHotPath = functionName === 'plugin' + || functionName === 'updates' + || functionName === 'stats' + || functionName === 'channel_self' + + appGlobal.use('*', (c, next): Promise => { + // ADD HEADER TO IDENTIFY WORKER SOURCE + const name = `${getEnv(c, 'ENV_NAME') || functionName}-${CapgoVersion}` + c.header('X-Worker-Source', name) + const hostname = new URL(c.req.url).hostname + if (!isPreviewHost(hostname)) + c.header('Content-Security-Policy', API_CONTENT_SECURITY_POLICY) + return next() + }) + + // Skip hono's access logger on plugin hot paths — it serializes every request + // on millions of device calls/day. + if (!pluginHotPath) + appGlobal.use('*', logger()) + // Use platform-specific request IDs, fallback to generated UUID. + // Do not cloudlog inside the generator: it runs on every request (incl. cf-ray). + appGlobal.use('*', requestId({ + generator: (c) => { + // Cloudflare provides the Ray ID in the cf-ray header + // Check this first as it's our primary deployment target + const cfRay = c.req.header('cf-ray') + if (cfRay) + return cfRay + // Supabase Edge Functions provide SB_EXECUTION_ID + const sbExecutionId = getEnv(c, 'SB_EXECUTION_ID') + if (sbExecutionId) + return sbExecutionId + // Fallback to crypto.randomUUID() if not on any known platform + return crypto.randomUUID() + }, + })) + + appGlobal.post('/ok', (c) => { + return c.json(BRES) + }) + appGlobal.post('/ko', (c) => { + const defaultResponse: SimpleErrorResponse = { + error: 'unknown_error', + message: 'KO', + moreInfo: {}, + } + return c.json(defaultResponse, 500) + }) + + return appGlobal +} diff --git a/benchmarks/pairs/agent-vfc/catalog-candidates.toml b/benchmarks/pairs/agent-vfc/catalog-candidates.toml new file mode 100644 index 0000000..d2649ab --- /dev/null +++ b/benchmarks/pairs/agent-vfc/catalog-candidates.toml @@ -0,0 +1,819 @@ +# CANDIDATES awaiting human review (reviewer = "pending"). Not loaded by load_pair_catalog. +# Set `reviewer` in recipes.toml after confirming vulnerability, mechanism, and license; then regenerate. + +# agent-vfc pair slice. Reviewed isolated functions with provenance. +# Honesty dashboard — not the local 100% CI gate. +# Do not add these names to LANGUAGE_MANIFESTS. +# Regenerated by benchmarks/pairs/catalog_gen.py from recipes.toml. + +[[pair]] +name = "anastasios-pw-session-14ec1a" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "Anas329796/Anastasios" +cve = "" +cwe = "CWE-918" +commit = "14ec1ac50f9e4e255d875236d30129c6207401d4" +parent = "adb7b0d5d6b81c46d56451c709e7eac9d0dc93fb" +commit_url = "https://github.com/Anas329796/Anastasios/commit/14ec1ac50f9e4e255d875236d30129c6207401d4" +vuln = "anastasios-pw-session-14ec1a/vuln.ts" +fixed = "anastasios-pw-session-14ec1a/fixed.ts" +relpath = "extensions/browser/src/browser/pw-session.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-918" +class = "server-side request forgery" +path = "extensions/browser/src/browser/pw-session.ts" +function = "isTopLevelNavigationRequest" +mechanism = "source_reaches_sink" +evidence = "fix(browser): harden SSRF redirect guard against non-navigation document hops [AI] (#62355)" + +[[pair]] +name = "astroclaw-pw-session-1553a8" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "P1R4N351/astroclaw" +cve = "" +cwe = "CWE-918" +commit = "1553a8adf123913cf7f1f1c75be4c4e5cd6c796b" +parent = "d3eed2dc01328f21dd404477bb28e2740fa371a4" +commit_url = "https://github.com/P1R4N351/astroclaw/commit/1553a8adf123913cf7f1f1c75be4c4e5cd6c796b" +vuln = "astroclaw-pw-session-1553a8/vuln.ts" +fixed = "astroclaw-pw-session-1553a8/fixed.ts" +relpath = "extensions/browser/src/browser/pw-session.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-918" +class = "server-side request forgery" +path = "extensions/browser/src/browser/pw-session.ts" +function = "isTopLevelNavigationRequest" +mechanism = "source_reaches_sink" +evidence = "fix(browser): harden SSRF redirect guard against non-navigation document hops [AI] (#62355)" + +[[pair]] +name = "capgo-app-hono-4dadd2" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "AGPL-3.0" +repo = "Cap-go/capgo.app" +cve = "" +cwe = "CWE-16" +commit = "4dadd2580d02213d227a331eee0232aa59859b59" +parent = "8c0ac7e233946abd8dc26581b871e9fcd0928899" +commit_url = "https://github.com/Cap-go/capgo.app/commit/4dadd2580d02213d227a331eee0232aa59859b59" +vuln = "capgo-app-hono-4dadd2/vuln.ts" +fixed = "capgo-app-hono-4dadd2/fixed.ts" +relpath = "supabase/functions/_backend/utils/hono.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-16" +class = "permissive default" +path = "supabase/functions/_backend/utils/hono.ts" +function = "next" +mechanism = "permissive_default" +evidence = "fix(api): CORS for Capacitor localhost WebView (#3105)" + +[[pair]] +name = "dnf-plugin-anyrepo-config-26d19a" +slice = "agent-vfc" +language = "python" +origin = "github-agent-trailer" +license = "Apache-2.0" +repo = "jfut/dnf-plugin-anyrepo" +cve = "" +cwe = "CWE-22" +commit = "26d19aeb8884f244e34ccfdc4f49f07055adc4d0" +parent = "33cde479d0db8f45dd89fbdcfb2bc0f1be66bd7a" +commit_url = "https://github.com/jfut/dnf-plugin-anyrepo/commit/26d19aeb8884f244e34ccfdc4f49f07055adc4d0" +vuln = "dnf-plugin-anyrepo-config-26d19a/vuln.py" +fixed = "dnf-plugin-anyrepo-config-26d19a/fixed.py" +relpath = "dnf_plugin_anyrepo/config.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-22" +class = "path traversal" +path = "dnf_plugin_anyrepo/config.py" +function = "validate_repo_name" +mechanism = "path_join_user_input" +evidence = "fix(config): prevent path traversal in repository names" + +[[pair]] +name = "edutrace-page-ee7ba2" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "Apache-2.0" +repo = "jardimdesoftware/edutrace" +cve = "" +cwe = "CWE-1321" +commit = "ee7ba25fb23ea89cc1b67549cb5e32c8ead92739" +parent = "c4dc6ddcc5492c77c8a5ad7369c87dea29c001fd" +commit_url = "https://github.com/jardimdesoftware/edutrace/commit/ee7ba25fb23ea89cc1b67549cb5e32c8ead92739" +vuln = "edutrace-page-ee7ba2/vuln.tsx" +fixed = "edutrace-page-ee7ba2/fixed.tsx" +relpath = "front/src/app/editar-anamnese/page.tsx" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-1321" +class = "prototype pollution" +path = "front/src/app/editar-anamnese/page.tsx" +function = "handleInputChange" +mechanism = "prototype_pollution" +evidence = "fix(front): impedir prototype pollution nos handlers de formulário aninhado" + +[[pair]] +name = "etude-lexical-markdown-editorconfig-c61a8b" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "dayflower/etude-lexical-markdown" +cve = "" +cwe = "CWE-1321" +commit = "c61a8bf78fe20d6ae0eab2e4b8b02004508525ed" +parent = "0a2184f59e79a04b1365cd49200026e8f2ffdbd5" +commit_url = "https://github.com/dayflower/etude-lexical-markdown/commit/c61a8bf78fe20d6ae0eab2e4b8b02004508525ed" +vuln = "etude-lexical-markdown-editorconfig-c61a8b/vuln.ts" +fixed = "etude-lexical-markdown-editorconfig-c61a8b/fixed.ts" +relpath = "src/config/editorConfig.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-1321" +class = "prototype pollution" +path = "src/config/editorConfig.ts" +function = "deepMergeInto" +mechanism = "prototype_pollution" +evidence = "fix: guard deepMergeInto against prototype pollution" + +[[pair]] +name = "etude-lexical-markdown-editorconfig-db4f54" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "dayflower/etude-lexical-markdown" +cve = "" +cwe = "CWE-1321" +commit = "db4f54b1ab80ad596de6425e84f90aa781c49999" +parent = "1c865815d05b072f4973388c3ac66b6696d9efde" +commit_url = "https://github.com/dayflower/etude-lexical-markdown/commit/db4f54b1ab80ad596de6425e84f90aa781c49999" +vuln = "etude-lexical-markdown-editorconfig-db4f54/vuln.ts" +fixed = "etude-lexical-markdown-editorconfig-db4f54/fixed.ts" +relpath = "src/config/editorConfig.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-1321" +class = "prototype pollution" +path = "src/config/editorConfig.ts" +function = "deepMergeInto" +mechanism = "prototype_pollution" +evidence = "fix: make prototype pollution guard recognizable by CodeQL" + +[[pair]] +name = "lensops-kubernetesmanagement-fe4910" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "amitknigam84/lensops" +cve = "" +cwe = "CWE-862" +commit = "fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84" +parent = "9b87ecc3a44a7fe2a5185cca1fe52441647d5748" +commit_url = "https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84" +vuln = "lensops-kubernetesmanagement-fe4910/vuln.tsx" +fixed = "lensops-kubernetesmanagement-fe4910/fixed.tsx" +relpath = "components/KubernetesManagement.tsx" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-862" +class = "missing authorization" +path = "components/KubernetesManagement.tsx" +function = "setErrorMessage" +mechanism = "missing_auth_guard" +evidence = "Fix missing auth header on namespaces API call" + +[[pair]] +name = "market-cap-route-4f8ae9" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "navaneethbv/market-cap" +cve = "" +cwe = "CWE-601" +commit = "4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6" +parent = "fec6f024ce72c9c47b61cbac6ce7a35cb2280602" +commit_url = "https://github.com/navaneethbv/market-cap/commit/4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6" +vuln = "market-cap-route-4f8ae9/vuln.ts" +fixed = "market-cap-route-4f8ae9/fixed.ts" +relpath = "app/auth/confirm/route.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-601" +class = "open redirect" +path = "app/auth/confirm/route.ts" +function = "GET" +mechanism = "source_reaches_sink" +evidence = "Fix open redirects on the auth next param" + +[[pair]] +name = "melodix-playerprovider-089931" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "alexshao3/melodix" +cve = "" +cwe = "CWE-16" +commit = "089931ef71ea1e5b7586520baf1a5498e2f05b13" +parent = "c074fc7413338aefcbea3170ef4d3ba486a8a4b7" +commit_url = "https://github.com/alexshao3/melodix/commit/089931ef71ea1e5b7586520baf1a5498e2f05b13" +vuln = "melodix-playerprovider-089931/vuln.tsx" +fixed = "melodix-playerprovider-089931/fixed.tsx" +relpath = "apps/miniapp/src/components/PlayerProvider.tsx" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-16" +class = "permissive default" +path = "apps/miniapp/src/components/PlayerProvider.tsx" +function = "PlayerProvider" +mechanism = "permissive_default" +evidence = "Merge pull request #17 from alexshao3/devin/1777216143-fix-audio-cors" + +[[pair]] +name = "mercado-do-vale-news-catalogmetadataservice-9b5fcf" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "handielson/Mercado-do-vale-news" +cve = "" +cwe = "CWE-16" +commit = "9b5fcf2dae41e56f5f97952266f2d6ba59598598" +parent = "70ca282b819ac1d60f4b5e665d224b1043ff1ccc" +commit_url = "https://github.com/handielson/Mercado-do-vale-news/commit/9b5fcf2dae41e56f5f97952266f2d6ba59598598" +vuln = "mercado-do-vale-news-catalogmetadataservice-9b5fcf/vuln.ts" +fixed = "mercado-do-vale-news-catalogmetadataservice-9b5fcf/fixed.ts" +relpath = "services/catalogMetadataService.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-16" +class = "permissive default" +path = "services/catalogMetadataService.ts" +function = "fetchMetadata" +mechanism = "permissive_default" +evidence = "fix(catalog): avoid metadata cors preflight" + +[[pair]] +name = "money-track-category-icons-4d4a5b" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "andrebubniak/money-track" +cve = "" +cwe = "CWE-1321" +commit = "4d4a5b063278c8b6697c3faae133d7c39fcd01c0" +parent = "add96df6d6a6b61506635c23dbacf6c6cfe24dc2" +commit_url = "https://github.com/andrebubniak/money-track/commit/4d4a5b063278c8b6697c3faae133d7c39fcd01c0" +vuln = "money-track-category-icons-4d4a5b/vuln.ts" +fixed = "money-track-category-icons-4d4a5b/fixed.ts" +relpath = "src/lib/category-icons.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-1321" +class = "prototype pollution" +path = "src/lib/category-icons.ts" +function = "isCategoryIcon" +mechanism = "prototype_pollution" +evidence = "fix(category-icons): use Object.hasOwn to prevent prototype pollution in type guard" + +[[pair]] +name = "nestjs-filter-normalizer-aa0e60" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "DavideCarvalho/nestjs-filter" +cve = "" +cwe = "CWE-1321" +commit = "aa0e600d05997ea3d77538ea1c2db427c439f2e8" +parent = "d899b40d3466f2ffbe9f01c4d49267412401f078" +commit_url = "https://github.com/DavideCarvalho/nestjs-filter/commit/aa0e600d05997ea3d77538ea1c2db427c439f2e8" +vuln = "nestjs-filter-normalizer-aa0e60/vuln.ts" +fixed = "nestjs-filter-normalizer-aa0e60/fixed.ts" +relpath = "packages/core/src/input/normalizer.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-1321" +class = "prototype pollution" +path = "packages/core/src/input/normalizer.ts" +function = "normalizeInput" +mechanism = "prototype_pollution" +evidence = "fix(core): add prototype pollution guard to normalizeInput" + +[[pair]] +name = "new-erp-final-app-6e9644" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "Apache-2.0" +repo = "Sachin-Salgar/NEW_ERP_FINAL" +cve = "" +cwe = "CWE-16" +commit = "6e9644c81ce3fb356669157182a5ef09a70c4b59" +parent = "08976c5d7a07b0663669b875b4507446199f85d7" +commit_url = "https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59" +vuln = "new-erp-final-app-6e9644/vuln.ts" +fixed = "new-erp-final-app-6e9644/fixed.ts" +relpath = "src/presentation/http/app.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-16" +class = "permissive default" +path = "src/presentation/http/app.ts" +function = "origin" +mechanism = "permissive_default" +evidence = "fix: allow patch requests through cors" + +[[pair]] +name = "nimestream-route-d17f53" +slice = "agent-vfc" +language = "javascript" +origin = "github-agent-trailer" +license = "MIT" +repo = "Irnhakim/nimestream" +cve = "" +cwe = "CWE-918" +commit = "d17f535bf3012041b056ff3aa991ff6cfa198e43" +parent = "0c37bfd9e5292b15b1d7af51f52793d9bdf1cef0" +commit_url = "https://github.com/Irnhakim/nimestream/commit/d17f535bf3012041b056ff3aa991ff6cfa198e43" +vuln = "nimestream-route-d17f53/vuln.js" +fixed = "nimestream-route-d17f53/fixed.js" +relpath = "app/api/img/route.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-918" +class = "server-side request forgery" +path = "app/api/img/route.js" +function = "GET" +mechanism = "source_reaches_sink" +evidence = "fix: resolve command injection and SSRF/Open Redirect security issues in multiScraper and image proxy API" + +[[pair]] +name = "openniw-jobs-0fc947" +slice = "agent-vfc" +language = "python" +origin = "github-agent-trailer" +license = "MIT" +repo = "HHHHHejia/openniw" +cve = "" +cwe = "CWE-639" +commit = "0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10" +parent = "4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc" +commit_url = "https://github.com/HHHHHejia/openniw/commit/0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10" +vuln = "openniw-jobs-0fc947/vuln.py" +fixed = "openniw-jobs-0fc947/fixed.py" +relpath = "backend/app/routers/jobs.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-639" +class = "insecure direct object reference" +path = "backend/app/routers/jobs.py" +function = "get_job" +mechanism = "identity_from_request_body" +evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership" + +[[pair]] +name = "openniw-jobs-b74de3" +slice = "agent-vfc" +language = "python" +origin = "github-agent-trailer" +license = "MIT" +repo = "ziyuzuo/openniw" +cve = "" +cwe = "CWE-639" +commit = "b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68" +parent = "98a0c392ba96cd2c4aa746dd6fc2b183f385225e" +commit_url = "https://github.com/ziyuzuo/openniw/commit/b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68" +vuln = "openniw-jobs-b74de3/vuln.py" +fixed = "openniw-jobs-b74de3/fixed.py" +relpath = "backend/app/routers/jobs.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-639" +class = "insecure direct object reference" +path = "backend/app/routers/jobs.py" +function = "get_job" +mechanism = "identity_from_request_body" +evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership" + +[[pair]] +name = "overleaf-with-claude-exportscontroller-3980b9" +slice = "agent-vfc" +language = "javascript" +origin = "github-agent-trailer" +license = "AGPL-3.0" +repo = "ZUENS2020/overleaf-with-claude" +cve = "" +cwe = "CWE-639" +commit = "3980b9e5806defb63e45ae2696e6c1899f3a8431" +parent = "5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed" +commit_url = "https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431" +vuln = "overleaf-with-claude-exportscontroller-3980b9/vuln.mjs" +fixed = "overleaf-with-claude-exportscontroller-3980b9/fixed.mjs" +relpath = "services/web/app/src/Features/Exports/ExportsController.mjs" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-639" +class = "insecure direct object reference" +path = "services/web/app/src/Features/Exports/ExportsController.mjs" +function = "exportProject" +mechanism = "identity_from_request_body" +evidence = "Fix IDOR in exports by adding token verification (Issue #31637) (#32883)" + +[[pair]] +name = "popmark-editorconfig-e5ac40" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "dayflower/popmark" +cve = "" +cwe = "CWE-1321" +commit = "e5ac407cef37c37178d440541ede301c838640fb" +parent = "31d1ac79b4cfa3118597348e79800a6561115149" +commit_url = "https://github.com/dayflower/popmark/commit/e5ac407cef37c37178d440541ede301c838640fb" +vuln = "popmark-editorconfig-e5ac40/vuln.ts" +fixed = "popmark-editorconfig-e5ac40/fixed.ts" +relpath = "src/lexical-markdown/config/editorConfig.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-1321" +class = "prototype pollution" +path = "src/lexical-markdown/config/editorConfig.ts" +function = "deepMergeInto" +mechanism = "prototype_pollution" +evidence = "fix: guard deepMergeInto against prototype pollution" + +[[pair]] +name = "rag-pipeline-utils-config-handler-76e5f0" +slice = "agent-vfc" +language = "javascript" +origin = "github-agent-trailer" +license = "MIT" +repo = "DevilsDev/rag-pipeline-utils" +cve = "" +cwe = "CWE-1321" +commit = "76e5f0a94d6803d0e463d26a61cedb7da90857a7" +parent = "ba587867118f703632ff2dc63c4feb8ad6793420" +commit_url = "https://github.com/DevilsDev/rag-pipeline-utils/commit/76e5f0a94d6803d0e463d26a61cedb7da90857a7" +vuln = "rag-pipeline-utils-config-handler-76e5f0/vuln.js" +fixed = "rag-pipeline-utils-config-handler-76e5f0/fixed.js" +relpath = "src/cli/handlers/config-handler.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-1321" +class = "prototype pollution" +path = "src/cli/handlers/config-handler.js" +function = "handleConfigSet" +mechanism = "prototype_pollution" +evidence = "fix(security): prevent prototype pollution in config-handler" + +[[pair]] +name = "registry-proxy-worker-426847" +slice = "agent-vfc" +language = "javascript" +origin = "github-agent-trailer" +license = "MIT" +repo = "agents-repo/registry-proxy" +cve = "" +cwe = "CWE-16" +commit = "426847899eb2ee94ae4e5a3aacc50423e36d2dc8" +parent = "6d5937972510cdb5ebca30607b538fdc2e280094" +commit_url = "https://github.com/agents-repo/registry-proxy/commit/426847899eb2ee94ae4e5a3aacc50423e36d2dc8" +vuln = "registry-proxy-worker-426847/vuln.js" +fixed = "registry-proxy-worker-426847/fixed.js" +relpath = "src/worker.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-16" +class = "permissive default" +path = "src/worker.js" +function = "buildUpstreamRequest" +mechanism = "permissive_default" +evidence = "fix: handle CORS OPTIONS preflight for catalog fetches (#10)" + +[[pair]] +name = "supplieradvisor-mvp-route-387bcf" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "bigfivefoods/supplieradvisor-mvp" +cve = "" +cwe = "CWE-639" +commit = "387bcf40e984a2ee1832ab7beabf0284c5b627d8" +parent = "a7a9bc260d649ea2e582c07435bfd7d93100647b" +commit_url = "https://github.com/bigfivefoods/supplieradvisor-mvp/commit/387bcf40e984a2ee1832ab7beabf0284c5b627d8" +vuln = "supplieradvisor-mvp-route-387bcf/vuln.ts" +fixed = "supplieradvisor-mvp-route-387bcf/fixed.ts" +relpath = "app/api/customers/claims/route.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-639" +class = "insecure direct object reference" +path = "app/api/customers/claims/route.ts" +function = "PATCH" +mechanism = "identity_from_request_body" +evidence = "Brief 62: Fix IDOR on customer_claims PATCH/DELETE (#156)" + +[[pair]] +name = "supplieradvisor-mvp-route-a69e8f" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "bigfivefoods/supplieradvisor-mvp" +cve = "" +cwe = "CWE-639" +commit = "a69e8f27c0f4d402573d16f0612b51155bbc46e1" +parent = "8558538471e4fb10856f8d9a17d2be5ab5c4fb9c" +commit_url = "https://github.com/bigfivefoods/supplieradvisor-mvp/commit/a69e8f27c0f4d402573d16f0612b51155bbc46e1" +vuln = "supplieradvisor-mvp-route-a69e8f/vuln.ts" +fixed = "supplieradvisor-mvp-route-a69e8f/fixed.ts" +relpath = "app/api/customers/opportunities/route.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-639" +class = "insecure direct object reference" +path = "app/api/customers/opportunities/route.ts" +function = "GET" +mechanism = "identity_from_request_body" +evidence = "Brief 60: Fix IDOR on opportunities GET/PATCH/DELETE (missing company membership checks) (#148)" + +[[pair]] +name = "tools-cli-tools-a96bc0" +slice = "agent-vfc" +language = "python" +origin = "github-agent-trailer" +license = "MIT" +repo = "D-sorganization/Tools" +cve = "" +cwe = "CWE-78" +commit = "a96bc0502a3dcdac303e4f0152508886e5c35c0a" +parent = "e735d28b82dd1fbc42c0ae01157b38d1b4c7c864" +commit_url = "https://github.com/D-sorganization/Tools/commit/a96bc0502a3dcdac303e4f0152508886e5c35c0a" +vuln = "tools-cli-tools-a96bc0/vuln.py" +fixed = "tools-cli-tools-a96bc0/fixed.py" +relpath = "src/shared/python/ai/tools/cli_tools.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "src/shared/python/ai/tools/cli_tools.py" +function = "_is_command_allowed" +mechanism = "source_reaches_sink" +evidence = "🛡️ Sentinel: [CRITICAL] Fix command injection in ShellTool (#3307)" + +[[pair]] +name = "trackly-cli-path-diagnostics-1c1aed" +slice = "agent-vfc" +language = "javascript" +origin = "github-agent-trailer" +license = "MIT" +repo = "trackly-app/trackly-cli" +cve = "" +cwe = "CWE-22" +commit = "1c1aed89b07edb04051830c8f086e02b65e961c6" +parent = "8d240ad8d7fc114c8f941ad956a2fdf32391ff60" +commit_url = "https://github.com/trackly-app/trackly-cli/commit/1c1aed89b07edb04051830c8f086e02b65e961c6" +vuln = "trackly-cli-path-diagnostics-1c1aed/vuln.js" +fixed = "trackly-cli-path-diagnostics-1c1aed/fixed.js" +relpath = "lib/path-diagnostics.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-22" +class = "path traversal" +path = "lib/path-diagnostics.js" +function = "inspectExistingAncestor" +mechanism = "path_join_user_input" +evidence = "fix(apply): report blocked path traversal" + +[[pair]] +name = "upstreamdrift-local-server-c3a078" +slice = "agent-vfc" +language = "python" +origin = "github-agent-trailer" +license = "MIT" +repo = "D-sorganization/UpstreamDrift" +cve = "" +cwe = "CWE-22" +commit = "c3a0787080489b30e2ed0abfac03b51e45da184b" +parent = "e28d1da5e103d93951ef11b78a6e9b89ac1fa2bc" +commit_url = "https://github.com/D-sorganization/UpstreamDrift/commit/c3a0787080489b30e2ed0abfac03b51e45da184b" +vuln = "upstreamdrift-local-server-c3a078/vuln.py" +fixed = "upstreamdrift-local-server-c3a078/fixed.py" +relpath = "src/api/local_server.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-22" +class = "path traversal" +path = "src/api/local_server.py" +function = "_find_logo_file" +mechanism = "path_join_user_input" +evidence = "fix(api): harden local server path traversal (#2805) (#2839)" + +[[pair]] +name = "waggledance-swarm-ssrf-host-guard-9a4548" +slice = "agent-vfc" +language = "python" +origin = "github-agent-trailer" +license = "Apache-2.0" +repo = "Ahkeratmehilaiset/waggledance-swarm" +cve = "" +cwe = "CWE-918" +commit = "9a454843aa35332ff2eb3106023eee0c4b5e444b" +parent = "934b939f1cca5259eaf807db3319eb97b90d8d80" +commit_url = "https://github.com/Ahkeratmehilaiset/waggledance-swarm/commit/9a454843aa35332ff2eb3106023eee0c4b5e444b" +vuln = "waggledance-swarm-ssrf-host-guard-9a4548/vuln.py" +fixed = "waggledance-swarm-ssrf-host-guard-9a4548/fixed.py" +relpath = "waggledance/core/v3_13_0/ssrf_host_guard.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-918" +class = "server-side request forgery" +path = "waggledance/core/v3_13_0/ssrf_host_guard.py" +function = "normalize_allowed_hosts" +mechanism = "source_reaches_sink" +evidence = "fix(ssrf): reject bare-string allowed hosts (#1460)" + +[[pair]] +name = "workers-research-utils-209444" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "G4brym/workers-research" +cve = "" +cwe = "CWE-89" +commit = "209444eb61cc564c2d6b43267256f85fe3e5e30e" +parent = "eab923ad3099c01b002fbd6be7cf994614bbdee4" +commit_url = "https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e" +vuln = "workers-research-utils-209444/vuln.ts" +fixed = "workers-research-utils-209444/fixed.ts" +relpath = "src/utils.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql injection" +path = "src/utils.ts" +function = "formatDuration" +mechanism = "source_reaches_sink" +evidence = "Fix SQL injection in search by using parameterized queries (#41)" + +[[pair]] +name = "zoonk-origin-bfbf67" +slice = "agent-vfc" +language = "typescript" +origin = "github-agent-trailer" +license = "MIT" +repo = "zoonk/zoonk" +cve = "" +cwe = "CWE-16" +commit = "bfbf670ccb41d63c407d32c51bd26e626a174f31" +parent = "2604269350bff51f58572c133b45010cb0976a5a" +commit_url = "https://github.com/zoonk/zoonk/commit/bfbf670ccb41d63c407d32c51bd26e626a174f31" +vuln = "zoonk-origin-bfbf67/vuln.ts" +fixed = "zoonk-origin-bfbf67/fixed.ts" +relpath = "packages/utils/src/origin.ts" +min_recall = 1.0 +fix_policy = "silent" +provenance = "agent" +split = "train" + +[[pair.expected]] +cwe = "CWE-16" +class = "permissive default" +path = "packages/utils/src/origin.ts" +function = "getAllowedHosts" +mechanism = "permissive_default" +evidence = "fix(api): allow staging cors from previews (#1559)" diff --git a/benchmarks/pairs/agent-vfc/catalog.toml b/benchmarks/pairs/agent-vfc/catalog.toml new file mode 100644 index 0000000..25c3a47 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/catalog.toml @@ -0,0 +1,4 @@ +# agent-vfc pair slice. Reviewed isolated functions with provenance. +# Honesty dashboard — not the local 100% CI gate. +# Do not add these names to LANGUAGE_MANIFESTS. +# Regenerated by benchmarks/pairs/catalog_gen.py from recipes.toml. diff --git a/benchmarks/pairs/agent-vfc/dnf-plugin-anyrepo-config-26d19a/fixed.py b/benchmarks/pairs/agent-vfc/dnf-plugin-anyrepo-config-26d19a/fixed.py new file mode 100644 index 0000000..8696d10 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/dnf-plugin-anyrepo-config-26d19a/fixed.py @@ -0,0 +1,26 @@ +# Provenance: jfut/dnf-plugin-anyrepo (fixed). +# repo: jfut/dnf-plugin-anyrepo +# commit: 26d19aeb8884f244e34ccfdc4f49f07055adc4d0 +# parent: 33cde479d0db8f45dd89fbdcfb2bc0f1be66bd7a +# commit_url: https://github.com/jfut/dnf-plugin-anyrepo/commit/26d19aeb8884f244e34ccfdc4f49f07055adc4d0 +# cve: +# license: Apache-2.0 +# function: validate_repo_name +# relpath: dnf_plugin_anyrepo/config.py +# provenance: agent +# mechanism: path_join_user_input + +def validate_repo_name(name: str) -> str: + """Validate the INI section name used as the repository alias.""" + + normalized = str(name).strip() + if not normalized: + raise ConfigError("repository name must not be empty") + if normalized == "main": + raise ConfigError("repository name must not be main") + # Keep the alias from resolving to the cache directory itself or its parent. + if normalized in {".", ".."}: + raise ConfigError(f"invalid repository name: {name}") + if any(char in normalized for char in "\x00\r\n[]/\\"): + raise ConfigError(f"invalid repository name: {name}") + return normalized diff --git a/benchmarks/pairs/agent-vfc/dnf-plugin-anyrepo-config-26d19a/vuln.py b/benchmarks/pairs/agent-vfc/dnf-plugin-anyrepo-config-26d19a/vuln.py new file mode 100644 index 0000000..4e5e292 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/dnf-plugin-anyrepo-config-26d19a/vuln.py @@ -0,0 +1,23 @@ +# Provenance: jfut/dnf-plugin-anyrepo (vuln). +# repo: jfut/dnf-plugin-anyrepo +# commit: 33cde479d0db8f45dd89fbdcfb2bc0f1be66bd7a +# parent: 33cde479d0db8f45dd89fbdcfb2bc0f1be66bd7a +# commit_url: https://github.com/jfut/dnf-plugin-anyrepo/commit/26d19aeb8884f244e34ccfdc4f49f07055adc4d0 +# cve: +# license: Apache-2.0 +# function: validate_repo_name +# relpath: dnf_plugin_anyrepo/config.py +# provenance: agent +# mechanism: path_join_user_input + +def validate_repo_name(name: str) -> str: + """Validate the INI section name used as the repository alias.""" + + normalized = str(name).strip() + if not normalized: + raise ConfigError("repository name must not be empty") + if normalized == "main": + raise ConfigError("repository name must not be main") + if any(char in normalized for char in "\r\n[]"): + raise ConfigError(f"invalid repository name: {name}") + return normalized diff --git a/benchmarks/pairs/agent-vfc/edutrace-page-ee7ba2/fixed.tsx b/benchmarks/pairs/agent-vfc/edutrace-page-ee7ba2/fixed.tsx new file mode 100644 index 0000000..905c9f3 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/edutrace-page-ee7ba2/fixed.tsx @@ -0,0 +1,442 @@ +// Provenance: jardimdesoftware/edutrace (fixed). +// repo: jardimdesoftware/edutrace +// commit: ee7ba25fb23ea89cc1b67549cb5e32c8ead92739 +// parent: c4dc6ddcc5492c77c8a5ad7369c87dea29c001fd +// commit_url: https://github.com/jardimdesoftware/edutrace/commit/ee7ba25fb23ea89cc1b67549cb5e32c8ead92739 +// cve: +// license: Apache-2.0 +// function: handleInputChange +// relpath: front/src/app/editar-anamnese/page.tsx +// provenance: agent +// mechanism: prototype_pollution + +function AnamnesePage() { + const searchParams = useSearchParams(); + const email = searchParams.get("email"); + const nome = searchParams.get("nome"); + + const { loading } = useAuth(); + const router = useRouter(); + + const [anamnesis, setAnamnesis] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const [formData, setFormData] = useState(initialAnamnesisState); + + useEffect(() => { + async function fetchData() { + setIsLoading(true); + try { + const token = decodeToken(); + if (!token) { + router.push('/login'); + return; + } + + const isStudent = token.id_level === ESTUDANTE; + + if (token.id_level === PROFISSIONAL_EDUCACAO || isStudent) { + router.push(`/anamnese${email ? `?email=${email}&nome=${nome}` : ''}`); + return; + } + + const target = isStudent ? token.email : email; + + if (target) { + const data = await getAnamneseByEmail(target); + setAnamnesis(data); + setFormData(data); + } + } catch (error) { + console.error("Anamnese não encontrada, iniciando modo de criação.", error); + setAnamnesis(null); + } finally { + setIsLoading(false); + } + } + + fetchData(); + }, [email, nome, router]); + + + const handleInputChange = (name: string, value: string) => { + const keys = name.split('.'); + if (keys.some((key) => key === '__proto__' || key === 'constructor' || key === 'prototype')) return; + + setFormData(prevData => { + const newData = JSON.parse(JSON.stringify(prevData)); + let current = newData; + for (let i = 0; i < keys.length - 1; i++) { + current = current[keys[i]]; + } + current[keys[keys.length - 1]] = value; + return newData; + }); + }; + + const handleCheckboxClick = (name: string) => { + const keys = name.split('.'); + if (keys.some((key) => key === '__proto__' || key === 'constructor' || key === 'prototype')) return; + + setFormData(prevData => { + // Criar uma cópia profunda para segurança + const newData = JSON.parse(JSON.stringify(prevData)); + + let currentSection = newData; + // Navega até o penúltimo nível do objeto + for (let i = 0; i < keys.length - 1; i++) { + currentSection = currentSection[keys[i]]; + } + + const finalKey = keys[keys.length - 1]; + const currentValue = currentSection[finalKey]; + + // Inverte o valor booleano atual + currentSection[finalKey] = !currentValue; + + return newData; + }); + }; + + const handleEdit = async () => { + if (!email) { + alert("Email do estudante não encontrado para editar anamnese."); + return; + } + + try { + const dadosAtualizados = { ...formData }; + await patchAnamnesis(dadosAtualizados, email); + alert("Anamnese atualizada com sucesso!"); + router.push(`/estudantes/visualizar?email=${email}&nome=${nome}`); + } catch (error) { + console.error("Erro ao atualizar Anamnese: ", error); + alert("Falha ao atualizar Anamnese. Verifique o console para mais detalhes."); + } + }; + + + if (isLoading || loading) { + return

Carregando...

; //ADICIONAR COMPONENTE DE LOADING + } + + if (anamnesis === null) { + return ( + +
+

O estudante ainda não possui uma Anamnese cadastrada

+ +
+
+ ) + } + + + return ( + +
+

Editar Anamnese

+ + {/* Identificação */} +
+

Identificação

+
+ handleInputChange('identification.nome_unidade_plena', e.target.value)} /> + handleInputChange('identification.nome_completo', e.target.value)} /> + handleInputChange('identification.data_de_nascimento', e.target.value)} /> + handleInputChange('identification.endereco', e.target.value)} /> + handleInputChange('identification.bairo', e.target.value)} /> + handleInputChange('identification.cep', e.target.value)} /> + handleInputChange('identification.municipio', e.target.value)} /> + handleInputChange('identification.curso', e.target.value)} /> + handleInputChange('identification.serie_de_escolaridade_atual', e.target.value)} /> + handleInputChange('identification.turma', e.target.value)} /> + handleInputChange('identification.idade_iniciou_estudos', e.target.value)} /> +
+
+ + {/* Dados Familiares */} +
+

Dados familiares

+
+ handleInputChange('family_data.nome_pai', e.target.value)} /> + handleInputChange('family_data.profissao_pai', e.target.value)} /> + handleInputChange('family_data.escolaridade_pai', e.target.value)} /> + handleInputChange('family_data.idade_pai', e.target.value)} /> + handleInputChange('family_data.nome_mae', e.target.value)} /> + handleInputChange('family_data.profissao_mae', e.target.value)} /> + handleInputChange('family_data.escolaridade_mae', e.target.value)} /> + handleInputChange('family_data.idade_mae', e.target.value)} /> + handleInputChange('family_data.outros_filhos', e.target.value)} /> +
+
+ +
+ handleCheckboxClick('family_data.uniao_pais.casados')} /> + handleCheckboxClick('family_data.uniao_pais.separados')} /> + handleCheckboxClick('family_data.uniao_pais.separados_como_nova_estrutura_familia')} /> +
+
+
+ +
+ handleCheckboxClick('family_data.pais.biologico')} /> + handleCheckboxClick('family_data.pais.adotivo')} /> +
+
+
+ handleInputChange('family_data.estudante_reside_com_quem', e.target.value)} /> + handleInputChange('family_data.reacao_estudante_situacao', e.target.value)} /> +
+
+ + {/* Condições familiares do estudante */} +
+

Condições familiares do estudante

+
+
+

Condições de moradia

+
+ handleCheckboxClick('family_conditions.moradia.taipa')} /> + handleCheckboxClick('family_conditions.moradia.alvenaria')} /> + handleCheckboxClick('family_conditions.moradia.palafita')} /> +
+
+
+

Convívio familiar

+
+ handleCheckboxClick('family_conditions.convivio_familiar.excelente')} /> + handleCheckboxClick('family_conditions.convivio_familiar.bom')} /> + handleCheckboxClick('family_conditions.convivio_familiar.problematico')} /> + handleCheckboxClick('family_conditions.convivio_familiar.precario')} /> +
+
+
+

Qualidade de comunicação

+
+ handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.execelente')} /> + handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.boa')} /> + handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.ruim')} /> + handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.pessima')} /> +
+
+
+
+ handleInputChange('family_conditions.medidas_disciplinares_com_estudante', e.target.value)} /> + handleInputChange('family_conditions.quem_usa_medidas_disciplinares', e.target.value)} /> + handleInputChange('family_conditions.reacao_estudante_frente_medidas', e.target.value)} /> + handleInputChange('family_conditions.reacao_contrariado', e.target.value)} /> +
+
+

Condições do ambiente familiar para a aprendizagem escolar

+ handleInputChange('family_conditions.condicao_ambiente_familiar_aprendizagem_escolar', e.target.value)} /> +
+
+ + {/* Histórico da Mãe */} +
+

Histórico da Mãe

+ + {/* Gestação */} +
+

Gestação

+
+ handleInputChange('mother_background.gestacao.transfusao_sanguinea_gravidez', e.target.value)} /> + handleInputChange('mother_background.gestacao.quando_sentiu_movimento_da_crianca', e.target.value)} /> + handleInputChange('mother_background.gestacao.levou_tombo_durante_gravidez', e.target.value)} /> + handleInputChange('mother_background.gestacao.doenca_na_gestacao', e.target.value)} /> + handleInputChange('mother_background.gestacao.condicao_saude_da_mae_na_gravidez', e.target.value)} /> + handleInputChange('mother_background.gestacao.episodio_marcante_gravidez', e.target.value)} /> +
+
+ + {/* Condições de Nascimento */} +
+

Condições de Nascimento

+
+ handleInputChange('mother_background.condicoes_nascimento.nasceu_quantos_meses', e.target.value)} /> + handleInputChange('mother_background.condicoes_nascimento.nasceu_quantos_quilos', e.target.value)} /> + handleInputChange('mother_background.condicoes_nascimento.nasceu_com_qual_comprimento', e.target.value)} /> + handleInputChange('mother_background.condicoes_nascimento.desenvolvimento_parto', e.target.value)} /> +
+
+ + {/* Primeiras Reações */} +
+

Primeiras Reações

+
+ handleInputChange('mother_background.primeiras_reacoes.chorou', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.ficou_vermelho_demais', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.ficou_vermelho_por_quanto_tempo', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.precisou_de_oxigenio', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.ficou_icterico', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.como_era_quando_bebe', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_afirmou_cabeca', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_sentou_sem_apoio', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_engatinhou', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_ficou_de_pe', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_andou', e.target.value)} /> +
+
+
+ + {/* Linguagem Verbal (3 anos) */} +
+

Linguagem Verbal (3 anos)

+
+
+ handleCheckboxClick('verbal_language_three_years.balbuciou')} /> + handleCheckboxClick('verbal_language_three_years.trocou_letras')} /> + handleCheckboxClick('verbal_language_three_years.gaguejou')} /> +
+ handleInputChange('verbal_language_three_years.primeiras_expressoes', e.target.value)} /> +
+
+ + {/* Desenvolvimento */} +
+

Desenvolvimento

+ + {/* Saúde */} +
+

Saúde

+
+ handleInputChange('development.saude.sofreu_acidente_ou_fez_cirurgia', e.target.value)} /> + handleInputChange('development.saude.possui_alergia', e.target.value)} /> + handleInputChange('development.saude.possui_bronquite_ou_asma', e.target.value)} /> + handleInputChange('development.saude.possui_problema_visao_audicao', e.target.value)} /> + handleInputChange('development.saude.ja_desmaiou', e.target.value)} /> + handleInputChange('development.saude.quando_desmaiou', e.target.value)} /> + handleInputChange('development.saude.teve_ou_tem_convulsoes', e.target.value)} /> +
+
+ + {/* Alimentação */} +
+

Alimentação

+
+ handleInputChange('development.alimentacao.foi_amamentada', e.target.value)} /> + handleInputChange('development.alimentacao.foi_amementada_ate_quando', e.target.value)} /> + handleInputChange('development.alimentacao.como_e_sua_alimentacao', e.target.value)} /> + handleInputChange('development.alimentacao.foi_forcado_se_alimentar', e.target.value)} /> + handleInputChange('development.alimentacao.recebe_ajuda_na_alimentacao', e.target.value)} /> +
+
+ + {/* Sono */} +
+

Sono

+
+
+ handleCheckboxClick('development.sono.dorme_bem')} /> + handleCheckboxClick('development.sono.dorme_separado_dos_pais')} /> +
+ handleInputChange('development.sono.com_quem_dorme', e.target.value)} /> +
+
+

Tipo de sono

+
+ handleCheckboxClick('development.sono.sono.agitado')} /> + handleCheckboxClick('development.sono.sono.tranquilo')} /> + handleCheckboxClick('development.sono.sono.fala_dormindo')} /> + handleCheckboxClick('development.sono.sono.sonambulo')} /> +
+
+
+
+ + {/* Informações Escolares */} +
+

Informações Escolares

+
+ handleInputChange('school_information.historico_escolar_comum_antecedentes_relevantes', e.target.value)} /> + handleInputChange('school_information.historico_escolar_especial_antecedentes_relevantes', e.target.value)} /> + handleInputChange('school_information.deficiencia_apresentada_estudante', e.target.value)} /> + handleInputChange('school_information.retido_alguma_vez', e.target.value)} /> + handleInputChange('school_information.gosta_de_ir_escola', e.target.value)} /> + handleInputChange('school_information.bem_aceito_pelos_amigous', e.target.value)} /> +
+
+ + {/* Sexualidade */} +
+

Sexualidade

+
+ handleCheckboxClick('sexuality.explanacao_sexual')} /> + handleCheckboxClick('sexuality.curiosidade_sexual')} /> + handleCheckboxClick('sexuality.conversa_com_pais_sobre_sexualidade')} /> +
+
+ + {/* Avaliação do Estudante */} +
+

Avaliação do Estudante

+
+ handleInputChange('student_assessment.estudante_apresenta_outro_tipo_deficiencia', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.apresenta_da_df_di_pc_tgd')} /> +
+ handleInputChange('student_assessment.se_sim_data_e_resultado_diagnostico', e.target.value)} /> + handleInputChange('student_assessment.se_não_situacao_quanto_diagnostico_tem_outras_dificuldades', e.target.value)} /> + handleInputChange('student_assessment.se_tem_outras_dificuldades', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.usa_medicamentos_controlados')} /> +
+ handleInputChange('student_assessment.usa_quais_medicamentos', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.medicamento_interfere_aprendizagem')} /> +
+ handleInputChange('student_assessment.se_intefere_aprendizagem', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.existem_recomendacoes_da_saude')} /> +
+ handleInputChange('student_assessment.se_possui_recomendacoes_da_saude', e.target.value)} /> +
+
+ + {/* Desenvolvimento do Estudante */} +
+

Desenvolvimento do Estudante

+ + {/* Função Cognitiva */} +
+

Função Cognitiva

+
+ handleInputChange('student_development.funcao_cognitiva.percepcao', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.atencao', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.memoria', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.linguagem', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.raciocinio_logico', e.target.value)} /> +
+
+ + {/* Função Motora */} +
+

Função Motora

+ handleInputChange('student_development.funcao_motora.desenvolvimento_e_capacidade_motora', e.target.value)} /> +
+ + {/* Função Pessoal Social */} +
+

Função Pessoal Social

+ handleInputChange('student_development.funcao_pressoal_social.area_emocional_afetiva_social', e.target.value)} /> +
+
+ + {/* Botões */} +
+ + +
+
+
+ ); +} diff --git a/benchmarks/pairs/agent-vfc/edutrace-page-ee7ba2/vuln.tsx b/benchmarks/pairs/agent-vfc/edutrace-page-ee7ba2/vuln.tsx new file mode 100644 index 0000000..20ccc9f --- /dev/null +++ b/benchmarks/pairs/agent-vfc/edutrace-page-ee7ba2/vuln.tsx @@ -0,0 +1,440 @@ +// Provenance: jardimdesoftware/edutrace (vuln). +// repo: jardimdesoftware/edutrace +// commit: c4dc6ddcc5492c77c8a5ad7369c87dea29c001fd +// parent: c4dc6ddcc5492c77c8a5ad7369c87dea29c001fd +// commit_url: https://github.com/jardimdesoftware/edutrace/commit/ee7ba25fb23ea89cc1b67549cb5e32c8ead92739 +// cve: +// license: Apache-2.0 +// function: handleInputChange +// relpath: front/src/app/editar-anamnese/page.tsx +// provenance: agent +// mechanism: prototype_pollution + +function AnamnesePage() { + const searchParams = useSearchParams(); + const email = searchParams.get("email"); + const nome = searchParams.get("nome"); + + const { loading } = useAuth(); + const router = useRouter(); + + const [anamnesis, setAnamnesis] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const [formData, setFormData] = useState(initialAnamnesisState); + + useEffect(() => { + async function fetchData() { + setIsLoading(true); + try { + const token = decodeToken(); + if (!token) { + router.push('/login'); + return; + } + + const isStudent = token.id_level === ESTUDANTE; + + if (token.id_level === PROFISSIONAL_EDUCACAO || isStudent) { + router.push(`/anamnese${email ? `?email=${email}&nome=${nome}` : ''}`); + return; + } + + const target = isStudent ? token.email : email; + + if (target) { + const data = await getAnamneseByEmail(target); + setAnamnesis(data); + setFormData(data); + } + } catch (error) { + console.error("Anamnese não encontrada, iniciando modo de criação.", error); + setAnamnesis(null); + } finally { + setIsLoading(false); + } + } + + fetchData(); + }, [email, nome, router]); + + + const handleInputChange = (name: string, value: string) => { + const keys = name.split('.'); + + setFormData(prevData => { + const newData = JSON.parse(JSON.stringify(prevData)); + let current = newData; + for (let i = 0; i < keys.length - 1; i++) { + current = current[keys[i]]; + } + current[keys[keys.length - 1]] = value; + return newData; + }); + }; + + const handleCheckboxClick = (name: string) => { + const keys = name.split('.'); + + setFormData(prevData => { + // Criar uma cópia profunda para segurança + const newData = JSON.parse(JSON.stringify(prevData)); + + let currentSection = newData; + // Navega até o penúltimo nível do objeto + for (let i = 0; i < keys.length - 1; i++) { + currentSection = currentSection[keys[i]]; + } + + const finalKey = keys[keys.length - 1]; + const currentValue = currentSection[finalKey]; + + // Inverte o valor booleano atual + currentSection[finalKey] = !currentValue; + + return newData; + }); + }; + + const handleEdit = async () => { + if (!email) { + alert("Email do estudante não encontrado para editar anamnese."); + return; + } + + try { + const dadosAtualizados = { ...formData }; + await patchAnamnesis(dadosAtualizados, email); + alert("Anamnese atualizada com sucesso!"); + router.push(`/estudantes/visualizar?email=${email}&nome=${nome}`); + } catch (error) { + console.error("Erro ao atualizar Anamnese: ", error); + alert("Falha ao atualizar Anamnese. Verifique o console para mais detalhes."); + } + }; + + + if (isLoading || loading) { + return

Carregando...

; //ADICIONAR COMPONENTE DE LOADING + } + + if (anamnesis === null) { + return ( + +
+

O estudante ainda não possui uma Anamnese cadastrada

+ +
+
+ ) + } + + + return ( + +
+

Editar Anamnese

+ + {/* Identificação */} +
+

Identificação

+
+ handleInputChange('identification.nome_unidade_plena', e.target.value)} /> + handleInputChange('identification.nome_completo', e.target.value)} /> + handleInputChange('identification.data_de_nascimento', e.target.value)} /> + handleInputChange('identification.endereco', e.target.value)} /> + handleInputChange('identification.bairo', e.target.value)} /> + handleInputChange('identification.cep', e.target.value)} /> + handleInputChange('identification.municipio', e.target.value)} /> + handleInputChange('identification.curso', e.target.value)} /> + handleInputChange('identification.serie_de_escolaridade_atual', e.target.value)} /> + handleInputChange('identification.turma', e.target.value)} /> + handleInputChange('identification.idade_iniciou_estudos', e.target.value)} /> +
+
+ + {/* Dados Familiares */} +
+

Dados familiares

+
+ handleInputChange('family_data.nome_pai', e.target.value)} /> + handleInputChange('family_data.profissao_pai', e.target.value)} /> + handleInputChange('family_data.escolaridade_pai', e.target.value)} /> + handleInputChange('family_data.idade_pai', e.target.value)} /> + handleInputChange('family_data.nome_mae', e.target.value)} /> + handleInputChange('family_data.profissao_mae', e.target.value)} /> + handleInputChange('family_data.escolaridade_mae', e.target.value)} /> + handleInputChange('family_data.idade_mae', e.target.value)} /> + handleInputChange('family_data.outros_filhos', e.target.value)} /> +
+
+ +
+ handleCheckboxClick('family_data.uniao_pais.casados')} /> + handleCheckboxClick('family_data.uniao_pais.separados')} /> + handleCheckboxClick('family_data.uniao_pais.separados_como_nova_estrutura_familia')} /> +
+
+
+ +
+ handleCheckboxClick('family_data.pais.biologico')} /> + handleCheckboxClick('family_data.pais.adotivo')} /> +
+
+
+ handleInputChange('family_data.estudante_reside_com_quem', e.target.value)} /> + handleInputChange('family_data.reacao_estudante_situacao', e.target.value)} /> +
+
+ + {/* Condições familiares do estudante */} +
+

Condições familiares do estudante

+
+
+

Condições de moradia

+
+ handleCheckboxClick('family_conditions.moradia.taipa')} /> + handleCheckboxClick('family_conditions.moradia.alvenaria')} /> + handleCheckboxClick('family_conditions.moradia.palafita')} /> +
+
+
+

Convívio familiar

+
+ handleCheckboxClick('family_conditions.convivio_familiar.excelente')} /> + handleCheckboxClick('family_conditions.convivio_familiar.bom')} /> + handleCheckboxClick('family_conditions.convivio_familiar.problematico')} /> + handleCheckboxClick('family_conditions.convivio_familiar.precario')} /> +
+
+
+

Qualidade de comunicação

+
+ handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.execelente')} /> + handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.boa')} /> + handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.ruim')} /> + handleCheckboxClick('family_conditions.qualidade_comunicacao_com_estudante.pessima')} /> +
+
+
+
+ handleInputChange('family_conditions.medidas_disciplinares_com_estudante', e.target.value)} /> + handleInputChange('family_conditions.quem_usa_medidas_disciplinares', e.target.value)} /> + handleInputChange('family_conditions.reacao_estudante_frente_medidas', e.target.value)} /> + handleInputChange('family_conditions.reacao_contrariado', e.target.value)} /> +
+
+

Condições do ambiente familiar para a aprendizagem escolar

+ handleInputChange('family_conditions.condicao_ambiente_familiar_aprendizagem_escolar', e.target.value)} /> +
+
+ + {/* Histórico da Mãe */} +
+

Histórico da Mãe

+ + {/* Gestação */} +
+

Gestação

+
+ handleInputChange('mother_background.gestacao.transfusao_sanguinea_gravidez', e.target.value)} /> + handleInputChange('mother_background.gestacao.quando_sentiu_movimento_da_crianca', e.target.value)} /> + handleInputChange('mother_background.gestacao.levou_tombo_durante_gravidez', e.target.value)} /> + handleInputChange('mother_background.gestacao.doenca_na_gestacao', e.target.value)} /> + handleInputChange('mother_background.gestacao.condicao_saude_da_mae_na_gravidez', e.target.value)} /> + handleInputChange('mother_background.gestacao.episodio_marcante_gravidez', e.target.value)} /> +
+
+ + {/* Condições de Nascimento */} +
+

Condições de Nascimento

+
+ handleInputChange('mother_background.condicoes_nascimento.nasceu_quantos_meses', e.target.value)} /> + handleInputChange('mother_background.condicoes_nascimento.nasceu_quantos_quilos', e.target.value)} /> + handleInputChange('mother_background.condicoes_nascimento.nasceu_com_qual_comprimento', e.target.value)} /> + handleInputChange('mother_background.condicoes_nascimento.desenvolvimento_parto', e.target.value)} /> +
+
+ + {/* Primeiras Reações */} +
+

Primeiras Reações

+
+ handleInputChange('mother_background.primeiras_reacoes.chorou', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.ficou_vermelho_demais', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.ficou_vermelho_por_quanto_tempo', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.precisou_de_oxigenio', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.ficou_icterico', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.como_era_quando_bebe', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_afirmou_cabeca', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_sentou_sem_apoio', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_engatinhou', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_ficou_de_pe', e.target.value)} /> + handleInputChange('mother_background.primeiras_reacoes.qual_idade_andou', e.target.value)} /> +
+
+
+ + {/* Linguagem Verbal (3 anos) */} +
+

Linguagem Verbal (3 anos)

+
+
+ handleCheckboxClick('verbal_language_three_years.balbuciou')} /> + handleCheckboxClick('verbal_language_three_years.trocou_letras')} /> + handleCheckboxClick('verbal_language_three_years.gaguejou')} /> +
+ handleInputChange('verbal_language_three_years.primeiras_expressoes', e.target.value)} /> +
+
+ + {/* Desenvolvimento */} +
+

Desenvolvimento

+ + {/* Saúde */} +
+

Saúde

+
+ handleInputChange('development.saude.sofreu_acidente_ou_fez_cirurgia', e.target.value)} /> + handleInputChange('development.saude.possui_alergia', e.target.value)} /> + handleInputChange('development.saude.possui_bronquite_ou_asma', e.target.value)} /> + handleInputChange('development.saude.possui_problema_visao_audicao', e.target.value)} /> + handleInputChange('development.saude.ja_desmaiou', e.target.value)} /> + handleInputChange('development.saude.quando_desmaiou', e.target.value)} /> + handleInputChange('development.saude.teve_ou_tem_convulsoes', e.target.value)} /> +
+
+ + {/* Alimentação */} +
+

Alimentação

+
+ handleInputChange('development.alimentacao.foi_amamentada', e.target.value)} /> + handleInputChange('development.alimentacao.foi_amementada_ate_quando', e.target.value)} /> + handleInputChange('development.alimentacao.como_e_sua_alimentacao', e.target.value)} /> + handleInputChange('development.alimentacao.foi_forcado_se_alimentar', e.target.value)} /> + handleInputChange('development.alimentacao.recebe_ajuda_na_alimentacao', e.target.value)} /> +
+
+ + {/* Sono */} +
+

Sono

+
+
+ handleCheckboxClick('development.sono.dorme_bem')} /> + handleCheckboxClick('development.sono.dorme_separado_dos_pais')} /> +
+ handleInputChange('development.sono.com_quem_dorme', e.target.value)} /> +
+
+

Tipo de sono

+
+ handleCheckboxClick('development.sono.sono.agitado')} /> + handleCheckboxClick('development.sono.sono.tranquilo')} /> + handleCheckboxClick('development.sono.sono.fala_dormindo')} /> + handleCheckboxClick('development.sono.sono.sonambulo')} /> +
+
+
+
+ + {/* Informações Escolares */} +
+

Informações Escolares

+
+ handleInputChange('school_information.historico_escolar_comum_antecedentes_relevantes', e.target.value)} /> + handleInputChange('school_information.historico_escolar_especial_antecedentes_relevantes', e.target.value)} /> + handleInputChange('school_information.deficiencia_apresentada_estudante', e.target.value)} /> + handleInputChange('school_information.retido_alguma_vez', e.target.value)} /> + handleInputChange('school_information.gosta_de_ir_escola', e.target.value)} /> + handleInputChange('school_information.bem_aceito_pelos_amigous', e.target.value)} /> +
+
+ + {/* Sexualidade */} +
+

Sexualidade

+
+ handleCheckboxClick('sexuality.explanacao_sexual')} /> + handleCheckboxClick('sexuality.curiosidade_sexual')} /> + handleCheckboxClick('sexuality.conversa_com_pais_sobre_sexualidade')} /> +
+
+ + {/* Avaliação do Estudante */} +
+

Avaliação do Estudante

+
+ handleInputChange('student_assessment.estudante_apresenta_outro_tipo_deficiencia', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.apresenta_da_df_di_pc_tgd')} /> +
+ handleInputChange('student_assessment.se_sim_data_e_resultado_diagnostico', e.target.value)} /> + handleInputChange('student_assessment.se_não_situacao_quanto_diagnostico_tem_outras_dificuldades', e.target.value)} /> + handleInputChange('student_assessment.se_tem_outras_dificuldades', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.usa_medicamentos_controlados')} /> +
+ handleInputChange('student_assessment.usa_quais_medicamentos', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.medicamento_interfere_aprendizagem')} /> +
+ handleInputChange('student_assessment.se_intefere_aprendizagem', e.target.value)} /> +
+ handleCheckboxClick('student_assessment.existem_recomendacoes_da_saude')} /> +
+ handleInputChange('student_assessment.se_possui_recomendacoes_da_saude', e.target.value)} /> +
+
+ + {/* Desenvolvimento do Estudante */} +
+

Desenvolvimento do Estudante

+ + {/* Função Cognitiva */} +
+

Função Cognitiva

+
+ handleInputChange('student_development.funcao_cognitiva.percepcao', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.atencao', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.memoria', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.linguagem', e.target.value)} /> + handleInputChange('student_development.funcao_cognitiva.raciocinio_logico', e.target.value)} /> +
+
+ + {/* Função Motora */} +
+

Função Motora

+ handleInputChange('student_development.funcao_motora.desenvolvimento_e_capacidade_motora', e.target.value)} /> +
+ + {/* Função Pessoal Social */} +
+

Função Pessoal Social

+ handleInputChange('student_development.funcao_pressoal_social.area_emocional_afetiva_social', e.target.value)} /> +
+
+ + {/* Botões */} +
+ + +
+
+
+ ); +} diff --git a/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-c61a8b/fixed.ts b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-c61a8b/fixed.ts new file mode 100644 index 0000000..0ae10af --- /dev/null +++ b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-c61a8b/fixed.ts @@ -0,0 +1,30 @@ +// Provenance: dayflower/etude-lexical-markdown (fixed). +// repo: dayflower/etude-lexical-markdown +// commit: c61a8bf78fe20d6ae0eab2e4b8b02004508525ed +// parent: 0a2184f59e79a04b1365cd49200026e8f2ffdbd5 +// commit_url: https://github.com/dayflower/etude-lexical-markdown/commit/c61a8bf78fe20d6ae0eab2e4b8b02004508525ed +// cve: +// license: MIT +// function: deepMergeInto +// relpath: src/config/editorConfig.ts +// provenance: agent +// mechanism: prototype_pollution + +function deepMergeInto( + target: Record, + source: Record, +): Record { + for (const key of Object.keys(source)) { + if (FORBIDDEN_KEYS.has(key)) continue; + const sourceValue = source[key]; + if (sourceValue === undefined) continue; + if (isPlainObject(sourceValue)) { + const targetValue = target[key]; + const nested = isPlainObject(targetValue) ? targetValue : {}; + target[key] = deepMergeInto(nested, sourceValue); + } else { + target[key] = sourceValue; + } + } + return target; +} diff --git a/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-c61a8b/vuln.ts b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-c61a8b/vuln.ts new file mode 100644 index 0000000..9aaa91d --- /dev/null +++ b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-c61a8b/vuln.ts @@ -0,0 +1,29 @@ +// Provenance: dayflower/etude-lexical-markdown (vuln). +// repo: dayflower/etude-lexical-markdown +// commit: 0a2184f59e79a04b1365cd49200026e8f2ffdbd5 +// parent: 0a2184f59e79a04b1365cd49200026e8f2ffdbd5 +// commit_url: https://github.com/dayflower/etude-lexical-markdown/commit/c61a8bf78fe20d6ae0eab2e4b8b02004508525ed +// cve: +// license: MIT +// function: deepMergeInto +// relpath: src/config/editorConfig.ts +// provenance: agent +// mechanism: prototype_pollution + +function deepMergeInto( + target: Record, + source: Record, +): Record { + for (const key of Object.keys(source)) { + const sourceValue = source[key]; + if (sourceValue === undefined) continue; + if (isPlainObject(sourceValue)) { + const targetValue = target[key]; + const nested = isPlainObject(targetValue) ? targetValue : {}; + target[key] = deepMergeInto(nested, sourceValue); + } else { + target[key] = sourceValue; + } + } + return target; +} diff --git a/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-db4f54/fixed.ts b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-db4f54/fixed.ts new file mode 100644 index 0000000..c65f4ec --- /dev/null +++ b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-db4f54/fixed.ts @@ -0,0 +1,30 @@ +// Provenance: dayflower/etude-lexical-markdown (fixed). +// repo: dayflower/etude-lexical-markdown +// commit: db4f54b1ab80ad596de6425e84f90aa781c49999 +// parent: 1c865815d05b072f4973388c3ac66b6696d9efde +// commit_url: https://github.com/dayflower/etude-lexical-markdown/commit/db4f54b1ab80ad596de6425e84f90aa781c49999 +// cve: +// license: MIT +// function: deepMergeInto +// relpath: src/config/editorConfig.ts +// provenance: agent +// mechanism: prototype_pollution + +function deepMergeInto( + target: Record, + source: Record, +): Record { + for (const key of Object.keys(source)) { + if (isForbiddenKey(key)) continue; + const sourceValue = source[key]; + if (sourceValue === undefined) continue; + if (isPlainObject(sourceValue)) { + const targetValue = target[key]; + const nested = isPlainObject(targetValue) ? targetValue : {}; + target[key] = deepMergeInto(nested, sourceValue); + } else { + target[key] = sourceValue; + } + } + return target; +} diff --git a/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-db4f54/vuln.ts b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-db4f54/vuln.ts new file mode 100644 index 0000000..f482e93 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/etude-lexical-markdown-editorconfig-db4f54/vuln.ts @@ -0,0 +1,30 @@ +// Provenance: dayflower/etude-lexical-markdown (vuln). +// repo: dayflower/etude-lexical-markdown +// commit: 1c865815d05b072f4973388c3ac66b6696d9efde +// parent: 1c865815d05b072f4973388c3ac66b6696d9efde +// commit_url: https://github.com/dayflower/etude-lexical-markdown/commit/db4f54b1ab80ad596de6425e84f90aa781c49999 +// cve: +// license: MIT +// function: deepMergeInto +// relpath: src/config/editorConfig.ts +// provenance: agent +// mechanism: prototype_pollution + +function deepMergeInto( + target: Record, + source: Record, +): Record { + for (const key of Object.keys(source)) { + if (FORBIDDEN_KEYS.has(key)) continue; + const sourceValue = source[key]; + if (sourceValue === undefined) continue; + if (isPlainObject(sourceValue)) { + const targetValue = target[key]; + const nested = isPlainObject(targetValue) ? targetValue : {}; + target[key] = deepMergeInto(nested, sourceValue); + } else { + target[key] = sourceValue; + } + } + return target; +} diff --git a/benchmarks/pairs/agent-vfc/lensops-kubernetesmanagement-fe4910/fixed.tsx b/benchmarks/pairs/agent-vfc/lensops-kubernetesmanagement-fe4910/fixed.tsx new file mode 100644 index 0000000..ba4c12d --- /dev/null +++ b/benchmarks/pairs/agent-vfc/lensops-kubernetesmanagement-fe4910/fixed.tsx @@ -0,0 +1,5842 @@ +// Provenance: amitknigam84/lensops (fixed). +// repo: amitknigam84/lensops +// commit: fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84 +// parent: 9b87ecc3a44a7fe2a5185cca1fe52441647d5748 +// commit_url: https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84 +// cve: +// license: MIT +// function: setErrorMessage +// relpath: components/KubernetesManagement.tsx +// provenance: agent +// mechanism: missing_auth_guard + +}: KubernetesManagementProps) { + const { token } = useAuth() + const [kubeconfig, setKubeconfig] = useState('') + + // Helper to get auth headers + const getAuthHeaders = () => { + const headers: HeadersInit = { 'Content-Type': 'application/json' } + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + return headers + } + const [isConnected, setIsConnected] = useState(false) + const [isConnecting, setIsConnecting] = useState(false) + const [isValidating, setIsValidating] = useState(false) + const [isResolving, setIsResolving] = useState(false) + const [validationResult, setValidationResult] = useState(null) + const [resolutionResult, setResolutionResult] = useState<{ + resolved: boolean + fixes: string[] + remainingIssues: string[] + resolvedKubeconfig: string + hasChanges: boolean + } | null>(null) + const [deployments, setDeployments] = useState([]) + const [services, setServices] = useState([]) + const [pods, setPods] = useState([]) + const [nodes, setNodes] = useState([]) + const [configmaps, setConfigmaps] = useState([]) + const [secrets, setSecrets] = useState([]) + const [events, setEvents] = useState([]) + const [cronjobs, setCronjobs] = useState([]) + const [jobs, setJobs] = useState([]) + const [ingresses, setIngresses] = useState([]) + const [pvcs, setPvcs] = useState([]) + const [namespaces, setNamespaces] = useState([]) + const [selectedNamespace, setSelectedNamespace] = useState('') + const [deployedNamespace, setDeployedNamespace] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [selectedDeployments, setSelectedDeployments] = useState>(new Set()) + const [imageUpdates, setImageUpdates] = useState>(new Map()) + const [isUpdating, setIsUpdating] = useState(false) + const [updateMode, setUpdateMode] = useState<'bulk' | 'selective'>('selective') + const [showDeployments, setShowDeployments] = useState(true) + const [showServices, setShowServices] = useState(true) + const [showPods, setShowPods] = useState(true) + const [showNamespaces, setShowNamespaces] = useState(true) + const [expandedPodIssues, setExpandedPodIssues] = useState(null) + const [expandedDeploymentIssues, setExpandedDeploymentIssues] = useState(null) + const [activeResourceTab, setActiveResourceTab] = useState<'overview' | 'deployments' | 'pods' | 'services' | 'configmaps' | 'secrets' | 'events' | 'cronjobs' | 'jobs' | 'ingresses' | 'pvcs' | 'flow-tracing' | 'databases' | 'download-images' | 'image-change-report' | 'mongodb-wizard' | 'adminer-wizard'>('overview') + const [selectedResource, setSelectedResource] = useState<{ + type: 'deployment' | 'service' | 'pod' | 'configmap' | 'secret' | 'cronjob' | 'job' | 'ingress' | 'pvc' | null + namespace: string + name: string + } | null>(null) + const [selectedEvent, setSelectedEvent] = useState(null) + const [actionModal, setActionModal] = useState<{ + type: 'scale' | 'restart' | 'delete' | null + resourceType: string + namespace: string + name: string + currentReplicas?: number + } | null>(null) + const [isActioning, setIsActioning] = useState(false) + const [scaleReplicas, setScaleReplicas] = useState(1) + const [selectedLogPod, setSelectedLogPod] = useState<{ + namespace: string + name: string + container?: string + } | null>(null) + const [describePod, setDescribePod] = useState<{ namespace: string; name: string } | null>(null) + const [describePodContent, setDescribePodContent] = useState(null) + const [describePodLoading, setDescribePodLoading] = useState(false) + const [describePodError, setDescribePodError] = useState(null) + const [showImageComparison, setShowImageComparison] = useState(false) + const [showBulkUpdateWizard, setShowBulkUpdateWizard] = useState(false) + const [bulkUpdateSelections, setBulkUpdateSelections] = useState>(new Set()) + const [bulkUpdateImage, setBulkUpdateImage] = useState('') + const [bulkUpdateStep, setBulkUpdateStep] = useState<'select' | 'preview'>('select') + const [portForwardModal, setPortForwardModal] = useState<{ service: Service; targetPort: number; localPort: number } | null>(null) + const [portForwardCopied, setPortForwardCopied] = useState(false) + const [showCreateSecretModal, setShowCreateSecretModal] = useState(false) + const [createSecretName, setCreateSecretName] = useState('') + const [createSecretData, setCreateSecretData] = useState<{ key: string; value: string }[]>([{ key: '', value: '' }]) + const [createSecretSaving, setCreateSecretSaving] = useState(false) + const [createSecretError, setCreateSecretError] = useState(null) + const [bulkUpdateTagOnly, setBulkUpdateTagOnly] = useState(false) + // Container-wise update state + const [bulkUpdateActiveTab, setBulkUpdateActiveTab] = useState('') + const [bulkUpdateContainerImages, setBulkUpdateContainerImages] = useState>(new Map()) + const [bulkUpdateContainerTagOnly, setBulkUpdateContainerTagOnly] = useState>(new Map()) + const [kubeconfigs, setKubeconfigs] = useState>([]) + const [imageChangeReportDate, setImageChangeReportDate] = useState(() => { + const now = new Date() + return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}` + }) + const [expandedImageHistoryDeployments, setExpandedImageHistoryDeployments] = useState>(new Set()) + const [imageChangeReportData, setImageChangeReportData] = useState<{ + date: string + byDeployment: Array<{ namespace: string; deployment: string; changeCount: number; images?: string[] }> + totalChanges: number + recentInNamespace?: Array<{ date: string; deployment: string; container?: string; image?: string; oldImage?: string; source?: string; user?: string; at: string }> + currentDeployments?: Array<{ + deployment: string + namespace: string + images: Array<{ container: string; image: string }> + lastUpdated?: string | null + changeCount?: number + imageFlow?: Array<{ createdAt: string; images: Array<{ container: string; image: string }>; revision?: string | number }> + }> + } | null>(null) + const [imageChangeReportLoading, setImageChangeReportLoading] = useState(false) + const [showKubeconfigManager, setShowKubeconfigManager] = useState(false) + const [newKubeconfigName, setNewKubeconfigName] = useState('') + const [newKubeconfigContent, setNewKubeconfigContent] = useState('') + const [isSavingKubeconfig, setIsSavingKubeconfig] = useState(false) + // Auto-refresh state + const [autoRefresh, setAutoRefresh] = useState(false) + const [refreshInterval, setRefreshInterval] = useState(10) // seconds + const [lastRefreshed, setLastRefreshed] = useState(null) + + // Overview, Search, Notifications state + const [auditLogs, setAuditLogs] = useState([]) + const [showSearchModal, setShowSearchModal] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [showNotificationPanel, setShowNotificationPanel] = useState(false) + + // Pod selection for bulk delete + const [selectedPods, setSelectedPods] = useState>(new Set()) + const [isDeletingPods, setIsDeletingPods] = useState(false) + + // Deleted deployments history for restore + const [deletedDeployments, setDeletedDeployments] = useState>([]) + const [showDeletedDeployments, setShowDeletedDeployments] = useState(false) + + // Load kubeconfig from manager settings or MongoDB (MongoDB is source of truth) + useEffect(() => { + // Priority: manager's personal kubeconfig > MongoDB active kubeconfig + if (managerKubeconfig) { + setKubeconfig(managerKubeconfig) + } + // MongoDB kubeconfigs will be loaded via loadSavedKubeconfigs + }, [managerKubeconfig]) + + // When a feature is disabled, switch away from its tab + useEffect(() => { + if (activeResourceTab === 'image-change-report' && !enableImageChangeReport) { + setActiveResourceTab('overview') + } + if (activeResourceTab === 'databases' && !enableDatabase) { + setActiveResourceTab('overview') + } + if (activeResourceTab === 'mongodb-wizard' && !enableMongoDBWizard) { + setActiveResourceTab('overview') + } + if (activeResourceTab === 'adminer-wizard' && !enableAdminerWizard) { + setActiveResourceTab('overview') + } + }, [enableImageChangeReport, enableDatabase, enableMongoDBWizard, enableAdminerWizard, activeResourceTab]) + + // Kubeconfig management removed - using ServiceAccount for cluster access + // No need to load saved kubeconfigs anymore + const loadSavedKubeconfigs = async () => { + // This function is kept for compatibility but does nothing + // Kubeconfig management has been removed in favor of ServiceAccount + console.log('ℹ️ Kubeconfig management removed - using ServiceAccount for cluster access') + } + + const handleSaveKubeconfig = async () => { + if (!newKubeconfigName.trim() || !newKubeconfigContent.trim()) { + setErrorMessage('Name and kubeconfig content are required') + return + } + + setIsSavingKubeconfig(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/kubeconfigs`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ + name: newKubeconfigName.trim(), + kubeconfig: newKubeconfigContent.trim(), + isActive: false + }) + }) + + const data = await response.json() + if (data.success) { + await loadSavedKubeconfigs() + setNewKubeconfigName('') + setNewKubeconfigContent('') + setShowKubeconfigManager(false) + setErrorMessage('✅ Kubeconfig saved successfully!') + setTimeout(() => setErrorMessage(null), 3000) + } else { + setErrorMessage(data.error || 'Failed to save kubeconfig') + } + } catch (error: any) { + setErrorMessage('Failed to save kubeconfig: ' + error.message) + } finally { + setIsSavingKubeconfig(false) + } + } + + const handleActivateKubeconfig = async (id: string) => { + setIsConnecting(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/kubeconfigs/${id}/activate`, { + method: 'POST', + headers: getAuthHeaders() + }) + + const data = await response.json() + if (data.success) { + setIsConnected(true) + setKubeconfig(data.kubeconfig.kubeconfig) + await loadSavedKubeconfigs() + setErrorMessage('✅ Successfully connected to cluster. Loading namespaces...') + try { + await loadNamespaces(true) + await loadData() + } catch (err) { + console.error('Error loading namespaces:', err) + } + setTimeout(() => { + setErrorMessage('✅ Successfully connected and loaded namespaces') + setTimeout(() => setErrorMessage(null), 3000) + }, 1000) + } else { + setErrorMessage(data.error || 'Failed to activate kubeconfig') + setIsConnected(false) + } + } catch (error: any) { + setErrorMessage('Failed to activate kubeconfig: ' + error.message) + setIsConnected(false) + } finally { + setIsConnecting(false) + } + } + + const handleDeleteKubeconfig = async (id: string) => { + if (!confirm('Are you sure you want to delete this kubeconfig?')) { + return + } + + try { + const response = await fetch(`${apiUrl}/k8s/kubeconfigs/${id}`, { + method: 'DELETE', + headers: getAuthHeaders() + }) + + const data = await response.json() + if (data.success) { + await loadSavedKubeconfigs() + // If deleted kubeconfig was active, clear connection + const wasActive = kubeconfigs.find(kc => kc.id === id)?.isActive + if (wasActive) { + setIsConnected(false) + setKubeconfig('') + } + } + } catch (error: any) { + setErrorMessage('Failed to delete kubeconfig: ' + error.message) + } + } + + // Clear validation/resolution results when kubeconfig changes + useEffect(() => { + if (validationResult) { + setValidationResult(null) + } + if (resolutionResult) { + setResolutionResult(null) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [kubeconfig]) + + // Set initial active tab when bulk update selections change + useEffect(() => { + if (showBulkUpdateWizard && bulkUpdateSelections.size > 0 && !bulkUpdateActiveTab) { + // Check if there are main containers or sidecar containers + let hasMainContainers = false + let hasSidecarContainers = false + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + const isSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecar) { + hasSidecarContainers = true + } else { + hasMainContainers = true + } + }) + } + }) + + // Set to 'main' if main containers exist, otherwise 'sidecar' if sidecar containers exist + if (hasMainContainers) { + setBulkUpdateActiveTab('main') + } else if (hasSidecarContainers) { + setBulkUpdateActiveTab('sidecar') + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showBulkUpdateWizard, bulkUpdateSelections.size]) + + const handleResolve = async () => { + if (!kubeconfig.trim()) { + setErrorMessage('Please paste or upload a kubeconfig file') + setResolutionResult(null) + return + } + + setIsResolving(true) + setErrorMessage(null) + setResolutionResult(null) + + try { + const response = await fetch(`${apiUrl}/k8s/resolve`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig }), + }) + + const data = await response.json() + + if (data.success) { + setResolutionResult({ + resolved: data.resolved, + fixes: data.fixes || [], + remainingIssues: data.remainingIssues || [], + resolvedKubeconfig: data.resolvedKubeconfig, + hasChanges: data.hasChanges, + }) + + if (data.resolved && data.hasChanges) { + setErrorMessage( + `✅ Resolved ${data.fixes.length} issue(s). Review the changes below and click "Apply Fixes" to update.` + ) + } else if (data.resolved) { + setErrorMessage('✅ No issues found to resolve') + } else { + setErrorMessage('⚠️ Some issues could not be auto-resolved') + } + + // Auto-validate the resolved config + if (data.resolvedKubeconfig) { + const validateResponse = await fetch(`${apiUrl}/k8s/validate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig: data.resolvedKubeconfig }), + }) + + const validateData = await validateResponse.json() + if (validateData.valid) { + setValidationResult({ + valid: true, + info: validateData.info, + }) + } else { + setValidationResult({ + valid: false, + issues: validateData.issues, + suggestions: validateData.suggestions, + info: validateData.info, + }) + } + } + } else { + setErrorMessage(data.message || 'Failed to resolve kubeconfig issues') + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to resolve kubeconfig') + } finally { + setIsResolving(false) + } + } + + const handleApplyFixes = () => { + if (resolutionResult && resolutionResult.resolvedKubeconfig) { + setKubeconfig(resolutionResult.resolvedKubeconfig) + setResolutionResult(null) + setErrorMessage('✅ Fixed kubeconfig applied. You can now validate or connect.') + setTimeout(() => setErrorMessage(null), 5000) + } + } + + const handleValidate = async () => { + if (!kubeconfig.trim()) { + setErrorMessage('Please paste or upload a kubeconfig file') + setValidationResult(null) + return + } + + setIsValidating(true) + setErrorMessage(null) + setValidationResult(null) + + try { + const response = await fetch(`${apiUrl}/k8s/validate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig }), + }) + + const data = await response.json() + + if (data.valid) { + setValidationResult({ + valid: true, + info: data.info, + }) + setErrorMessage('✅ Kubeconfig is valid and ready to connect') + setTimeout(() => setErrorMessage(null), 5000) + } else { + setValidationResult({ + valid: false, + issues: data.issues || [], + suggestions: data.suggestions || [], + info: data.info, + }) + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to validate kubeconfig') + setValidationResult({ + valid: false, + issues: ['Validation request failed'], + suggestions: ['Check your connection to the API server'], + }) + } finally { + setIsValidating(false) + } + } + + const handleConnect = async () => { + if (!kubeconfig.trim()) { + setErrorMessage('Please paste or upload a kubeconfig file') + return + } + + setIsConnecting(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/connect`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig }), + }) + + const data = await response.json() + + if (response.ok && data.success) { + setIsConnected(true) + setValidationResult({ + valid: true, + info: data.info, + }) + + // Save kubeconfig to MongoDB for retention (MongoDB is source of truth) + try { + const saveResponse = await fetch(`${apiUrl}/k8s/kubeconfigs`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ + name: `Kubeconfig ${new Date().toLocaleString()}`, + kubeconfig: kubeconfig, + isActive: true // Set as active since user just connected with it + }) + }) + const saveData = await saveResponse.json() + if (saveData.success) { + console.log('✅ Kubeconfig saved to MongoDB for retention') + // Reload kubeconfigs list to show the newly saved one + await loadSavedKubeconfigs() + // Clear localStorage since MongoDB is now the source of truth + storage.removeLocal(STORAGE_KEYS.KUBECONFIG) + } + } catch (saveError) { + console.error('Failed to save kubeconfig to MongoDB:', saveError) + // Fallback to localStorage only if MongoDB save fails + storage.setLocal(STORAGE_KEYS.KUBECONFIG, kubeconfig) + } + + setErrorMessage('✅ Successfully connected to cluster. Kubeconfig saved to MongoDB. Loading namespaces...') + // Load namespaces immediately after connection (skip connection check since we just connected) + try { + await loadNamespaces(true) + // Always load data after namespaces are loaded (defaults to 'all' to show all resources) + await loadData() + } catch (err) { + console.error('Error loading namespaces after connection:', err) + setErrorMessage('✅ Connected, but failed to load namespaces. Click "Load Namespaces" to retry.') + } + setTimeout(() => { + if (namespaces.length > 0) { + setErrorMessage('✅ Successfully connected and loaded namespaces') + } else { + setErrorMessage('✅ Connected. Click "Load Namespaces" to fetch namespaces.') + } + setTimeout(() => setErrorMessage(null), 5000) + }, 1000) + } else { + // Handle validation errors or connection errors + if (data.issues && data.suggestions) { + setValidationResult({ + valid: data.valid || false, + issues: data.issues, + suggestions: data.suggestions, + info: data.info, + }) + } + setErrorMessage(data.message || 'Failed to connect to cluster') + setIsConnected(false) + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to connect to cluster') + setIsConnected(false) + } finally { + setIsConnecting(false) + } + } + + const loadNamespaces = async (skipConnectionCheck = false) => { + if (!skipConnectionCheck && !isConnected) { + setErrorMessage('Please connect to a Kubernetes cluster first') + return + } + + setIsLoading(true) + setErrorMessage(null) + + try { + console.log('📋 Loading namespaces from:', `${apiUrl}/k8s/namespaces`) + const response = await fetch(`${apiUrl}/k8s/namespaces`, { + headers: token ? { 'Authorization': `Bearer ${token}` } : {}, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: 'Unknown error' })) + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + console.log('📋 Namespaces response:', data) + + if (data.success && data.namespaces) { + setNamespaces(data.namespaces) + setIsConnected(true) // ✅ Mark as connected after successfully loading namespaces + + // Always use deployed namespace (from ServiceAccount) + const namespaceToUse = data.podNamespace || (data.namespaces.length > 0 ? data.namespaces[0].name : null); + + if (namespaceToUse) { + setDeployedNamespace(namespaceToUse) + setSelectedNamespace(namespaceToUse) + console.log('📌 Using deployed namespace:', namespaceToUse) + // Automatically load resources for the deployed namespace + console.log('📦 Loading resources from deployed namespace:', namespaceToUse) + setTimeout(() => { + loadData() + }, 100) + } else { + setErrorMessage('⚠️ No namespace found. Please check POD_NAMESPACE configuration.') + } + } else { + setErrorMessage(data.message || data.error || 'Failed to load namespaces') + } + } catch (error: any) { + console.error('❌ Failed to load namespaces:', error) + setErrorMessage(error.message || 'Failed to load namespaces. Please check your connection and try again.') + } finally { + setIsLoading(false) + } + } + + const loadNodes = async () => { + if (!isConnected) return + try { + const response = await fetch(`${apiUrl}/k8s/nodes`) + const data = await response.json() + if (response.ok && data?.nodes) { + setNodes(data.nodes) + } else { + console.warn('⚠️ Failed to load nodes:', data?.message || data?.error) + setNodes([]) + } + } catch (error: any) { + console.error('❌ Failed to load nodes:', error) + setNodes([]) + } + } + + const loadImageChangeReport = async () => { + if (!apiUrl) return + const namespaceToUse = deployedNamespace || selectedNamespace + if (!namespaceToUse || namespaceToUse === 'all') { + setErrorMessage('No namespace available. Connect to the cluster so the report is scoped to your deployed namespace.') + return + } + setImageChangeReportLoading(true) + setImageChangeReportData(null) + setErrorMessage(null) + try { + const params = new URLSearchParams({ date: imageChangeReportDate, namespace: namespaceToUse }) + const response = await fetch(`${apiUrl}/k8s/image-change-report?${params}`) + const data = await response.json() + if (response.ok && data.success) { + setImageChangeReportData({ + date: data.date, + byDeployment: data.byDeployment || [], + totalChanges: data.totalChanges ?? 0, + recentInNamespace: data.recentInNamespace || [], + currentDeployments: data.currentDeployments || [], + }) + } else { + setErrorMessage(data.message || 'Failed to load image change report') + } + } catch (err: any) { + setErrorMessage(err.message || 'Failed to load image change report') + } finally { + setImageChangeReportLoading(false) + } + } + + const loadData = async () => { + if (!isConnected) { + console.log('⚠️ Not connected, skipping loadData') + return + } + + // Always use deployed namespace (from ServiceAccount) + const namespaceToUse = deployedNamespace || selectedNamespace + if (!namespaceToUse || namespaceToUse === 'all') { + console.log('⚠️ No namespace available, waiting for namespace detection...') + // If we have apiUrl but no namespace yet, try to load namespaces + if (apiUrl && !deployedNamespace) { + loadNamespaces(true) + } + // Don't clear existing data if we're just waiting for namespace + // This prevents "no resources" flash when namespace is temporarily unavailable + return + } + + setIsLoading(true) + setErrorMessage(null) + + // Don't clear data here - let the API responses determine what to show + // This prevents "no resources" flash during refresh + + try { + // Always use deployed namespace + const namespaceParam = namespaceToUse + console.log('📦 Loading resources from deployed namespace:', namespaceParam) + + const [deploymentsRes, servicesRes, podsRes, configmapsRes, secretsRes, eventsRes, cronjobsRes, jobsRes, ingressesRes, pvcsRes] = await Promise.all([ + fetch(`${apiUrl}/k8s/deployments?namespace=${namespaceParam}`), + fetch(`${apiUrl}/k8s/services?namespace=${namespaceParam}`), + fetch(`${apiUrl}/k8s/pods?namespace=${namespaceParam}`), + fetch(`${apiUrl}/k8s/configmaps?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ ConfigMaps fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, configmaps: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/secrets?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Secrets fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, secrets: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/events?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Events fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, events: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/cronjobs?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ CronJobs fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, cronjobs: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/jobs?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Jobs fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, jobs: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/ingresses?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Ingresses fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, ingresses: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/pvcs?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ PVCs fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, pvcs: [], error: err.message }) }; + }), + ]) + + const deploymentsData = await deploymentsRes.json() + const servicesData = await servicesRes.json() + const podsData = await podsRes.json() + + console.log('📦 Deployments response:', deploymentsData) + console.log('📦 Services response:', servicesData) + console.log('📦 Pods response:', podsData) + + if (deploymentsData.success) { + console.log('✅ Setting deployments:', deploymentsData.deployments?.length || 0) + setDeployments(deploymentsData.deployments || []) + } else { + console.error('❌ Deployments API error:', deploymentsData.error || deploymentsData.message) + // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues + // This prevents "no resources" flash during refresh when there are transient errors + if (deploymentsRes.status >= 400 && deploymentsRes.status < 500) { + setDeployments([]) + } else { + console.warn('⚠️ Keeping existing deployments due to server/network error') + } + } + + if (servicesData.success) { + console.log('✅ Setting services:', servicesData.services?.length || 0) + setServices(servicesData.services || []) + } else { + console.error('❌ Services API error:', servicesData.error || servicesData.message) + // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues + if (servicesRes.status >= 400 && servicesRes.status < 500) { + setServices([]) + } else { + console.warn('⚠️ Keeping existing services due to server/network error') + } + } + + if (podsData.success) { + const podsArray = podsData.pods || [] + + // Remove duplicates based on namespace + name + const uniquePods = podsArray.reduce((acc: Map, pod: Pod) => { + const key = `${pod.namespace}/${pod.name}` + if (!acc.has(key)) { + acc.set(key, pod) + } else { + console.warn(`⚠️ Duplicate pod found: ${key}`) + } + return acc + }, new Map()) + + const deduplicatedPods: Pod[] = Array.from(uniquePods.values()) + + // Filter by namespace if a specific namespace is selected (not 'all') + const filteredPods: Pod[] = selectedNamespace && selectedNamespace !== 'all' + ? deduplicatedPods.filter((pod: Pod) => pod.namespace === selectedNamespace) + : deduplicatedPods + + console.log('✅ Setting pods:', { + original: podsArray.length, + afterDedup: deduplicatedPods.length, + afterFilter: filteredPods.length, + namespace: namespaceParam, + selectedNamespace: selectedNamespace + }) + + if (podsArray.length !== filteredPods.length) { + console.warn(`⚠️ Pod count mismatch: API returned ${podsArray.length}, displaying ${filteredPods.length}`) + } + + setPods(filteredPods) + } else { + console.error('❌ Pods API error:', podsData.error || podsData.message) + // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues + if (podsRes.status >= 400 && podsRes.status < 500) { + setPods([]) + } else { + console.warn('⚠️ Keeping existing pods due to server/network error') + } + } + + const configmapsData = await configmapsRes.json() + if (configmapsData.success) { + console.log('✅ Setting configmaps:', configmapsData.configmaps?.length || 0) + setConfigmaps(configmapsData.configmaps || []) + } else { + console.error('❌ ConfigMaps API error:', configmapsData.error || configmapsData.message) + setConfigmaps([]) + } + + const secretsData = await secretsRes.json() + if (secretsData.success) { + setSecrets(secretsData.secrets || []) + } else { + setSecrets([]) + } + + const eventsData = await eventsRes.json() + if (eventsData.success) { + console.log('✅ Setting events:', eventsData.events?.length || 0) + setEvents(eventsData.events || []) + } else { + console.error('❌ Events API error:', eventsData.error || eventsData.message) + setEvents([]) + } + + // Handle CronJobs + try { + const cronjobsData = await cronjobsRes.json() + if (cronjobsData.success) { + console.log('✅ Setting cronjobs:', cronjobsData.cronjobs?.length || 0) + setCronjobs(cronjobsData.cronjobs || []) + } else { + console.error('❌ CronJobs API error:', cronjobsData.error || cronjobsData.message, cronjobsData.details) + // Don't show error if it's just that there are no cronjobs (404 or empty list) + if (cronjobsRes.status !== 404 && !cronjobsData.message?.includes('not found')) { + console.warn('⚠️ CronJobs fetch warning:', cronjobsData.message) + } + setCronjobs([]) + } + } catch (cronjobsError: any) { + console.error('❌ Failed to parse CronJobs response:', cronjobsError) + setCronjobs([]) + } + + // Handle Jobs + try { + const jobsData = await jobsRes.json() + if (jobsData.success) { + console.log('✅ Setting jobs:', jobsData.jobs?.length || 0) + setJobs(jobsData.jobs || []) + } else { + console.error('❌ Jobs API error:', jobsData.error || jobsData.message, jobsData.details) + // Don't show error if it's just that there are no jobs (404 or empty list) + if (jobsRes.status !== 404 && !jobsData.message?.includes('not found')) { + console.warn('⚠️ Jobs fetch warning:', jobsData.message) + } + setJobs([]) + } + } catch (jobsError: any) { + console.error('❌ Failed to parse Jobs response:', jobsError) + setJobs([]) + } + + // Handle Ingresses + try { + const ingressesData = await ingressesRes.json() + if (ingressesData.success) { + setIngresses(ingressesData.ingresses || []) + } else { + setIngresses([]) + } + } catch (ingressesError: any) { + console.error('❌ Failed to parse Ingresses response:', ingressesError) + setIngresses([]) + } + + // Handle PVCs + try { + const pvcsData = await pvcsRes.json() + if (pvcsData.success) { + setPvcs(pvcsData.pvcs || []) + } else { + setPvcs([]) + } + } catch (pvcsError: any) { + console.error('❌ Failed to parse PVCs response:', pvcsError) + setPvcs([]) + } + + // Check for HTTP errors + if (!deploymentsRes.ok) { + console.error('❌ Deployments HTTP error:', deploymentsRes.status, deploymentsRes.statusText) + setErrorMessage(`Failed to fetch deployments: ${deploymentsRes.status} ${deploymentsRes.statusText}`) + } + if (!servicesRes.ok) { + console.error('❌ Services HTTP error:', servicesRes.status, servicesRes.statusText) + setErrorMessage(`Failed to fetch services: ${servicesRes.status} ${servicesRes.statusText}`) + } + if (!podsRes.ok) { + console.error('❌ Pods HTTP error:', podsRes.status, podsRes.statusText) + setErrorMessage(`Failed to fetch pods: ${podsRes.status} ${podsRes.statusText}`) + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to load cluster data') + } finally { + setIsLoading(false) + setLastRefreshed(new Date()) + } + } + + + // Automatically load namespaces and resources on mount when apiUrl is available + useEffect(() => { + if (apiUrl && !isConnected && !isConnecting) { + console.log('🚀 Auto-connecting to Kubernetes cluster via ServiceAccount...') + loadNamespaces(true) // Skip connection check since we're auto-connecting + } + }, [apiUrl]) // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (isConnected && selectedNamespace) { + console.log('🔄 Namespace changed to:', selectedNamespace, '- Loading resources...') + // Clear previous data when namespace changes + setDeployments([]) + setPods([]) + setNodes([]) + setServices([]) + setConfigmaps([]) + setEvents([]) + setCronjobs([]) + setJobs([]) + // Load new data for the selected namespace + loadData() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedNamespace, isConnected]) + + useEffect(() => { + if (isConnected) { + loadNodes() + } else { + setNodes([]) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected]) + + // Auto-refresh effect + useEffect(() => { + if (!autoRefresh || !isConnected) return + + const interval = setInterval(async () => { + console.log('🔄 Auto-refreshing data...') + const currentTab = activeResourceTab + await loadData() + setActiveResourceTab(currentTab) + setLastRefreshed(new Date()) + }, refreshInterval * 1000) + + return () => clearInterval(interval) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoRefresh, refreshInterval, isConnected, deployedNamespace, selectedNamespace]) + + const handleImageChange = ( + namespace: string, + deploymentName: string, + containerName: string, + newImage: string, + originalImage: string + ) => { + const key = `${namespace}/${deploymentName}/${containerName}` + const newUpdates = new Map(imageUpdates) + + if (newImage === originalImage) { + newUpdates.delete(key) + } else { + newUpdates.set(key, { + namespace: namespace, + deployment: deploymentName, + container: containerName, + image: newImage, + originalImage, + }) + } + + setImageUpdates(newUpdates) + } + + // Helper function to extract base image (without tag) + const getBaseImage = (image: string): string => { + // Remove tag if present (format: image:tag) + if (image.includes(':')) { + // Check if it's a port number (registry:port/image:tag) + const parts = image.split(':') + if (parts.length > 2) { + // Has port, get everything before the last colon + const lastColonIndex = image.lastIndexOf(':') + return image.substring(0, lastColonIndex) + } else { + // Simple format: image:tag + return parts[0] + } + } + // Remove digest if present (format: image@sha256:...) + if (image.includes('@')) { + return image.split('@')[0] + } + return image + } + + // Helper function to apply tag-only update + const applyTagOnlyUpdate = (originalImage: string, newTag: string): string => { + const baseImage = getBaseImage(originalImage) + return `${baseImage}:${newTag}` + } + + const handleBulkImageUpdate = (newImage: string) => { + const newUpdates = new Map() + + deployments.forEach((deployment) => { + deployment.containers.forEach((container) => { + const key = `${selectedNamespace}/${deployment.name}/${container.name}` + newUpdates.set(key, { + namespace: selectedNamespace, + deployment: deployment.name, + container: container.name, + image: newImage, + originalImage: container.image, + }) + }) + }) + + setImageUpdates(newUpdates) + } + + const handleSaveSingleImage = async (namespace: string, deployment: string, container: string, newImage: string, originalImage: string) => { + if (newImage === originalImage) { + setErrorMessage('Image unchanged') + setTimeout(() => setErrorMessage(null), 2000) + return + } + + if (!newImage || newImage.trim() === '') { + setErrorMessage('Please enter a valid image name') + setTimeout(() => setErrorMessage(null), 2000) + return + } + + // Validate required fields + const trimmedNamespace = namespace?.trim() + const trimmedDeployment = deployment?.trim() + const trimmedContainer = container?.trim() + const trimmedImage = newImage.trim() + + if (!trimmedNamespace || !trimmedDeployment || !trimmedContainer) { + setErrorMessage('Missing required fields: namespace, deployment, or container name') + setTimeout(() => setErrorMessage(null), 3000) + return + } + + // Ensure deployment name doesn't contain namespace (some APIs return it as namespace/name) + let deploymentName = trimmedDeployment + if (trimmedDeployment.includes('/')) { + const parts = trimmedDeployment.split('/').filter(p => p && p.trim() !== '') + deploymentName = parts.length > 0 ? parts[parts.length - 1] : trimmedDeployment + console.log('Extracted deployment name:', { original: trimmedDeployment, extracted: deploymentName, parts }) + } + + // Final validation after extraction + if (!deploymentName || deploymentName.trim() === '') { + setErrorMessage('Invalid deployment name after processing') + setTimeout(() => setErrorMessage(null), 3000) + return + } + + setIsUpdating(true) + setErrorMessage(null) + + try { + const updatePayload = { + namespace: String(trimmedNamespace), + deployment: String(deploymentName), + container: String(trimmedContainer), + image: String(trimmedImage) + } + + console.log('Saving image update:', { + namespace: trimmedNamespace, + deployment: deploymentName, + container: trimmedContainer, + image: trimmedImage, + originalDeployment: deployment, + payload: updatePayload + }) + + const response = await fetch(`${apiUrl}/k8s/update-images`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + updates: [updatePayload], + source: 'ui-single' + }), + }) + + // Check if response is ok before parsing JSON + if (!response.ok) { + const errorText = await response.text() + let errorData + try { + errorData = JSON.parse(errorText) + } catch { + errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } + } + throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success || (data.updated && data.updated > 0)) { + setErrorMessage(`✅ Successfully updated ${container} image in ${deployment}`) + // Remove from pending updates + const key = `${namespace}/${deployment}/${container}` + const newUpdates = new Map(imageUpdates) + newUpdates.delete(key) + setImageUpdates(newUpdates) + await loadData() + setTimeout(() => setErrorMessage(null), 5000) + } else { + // Check if there are errors in the response + const errorMsg = data.errors && data.errors.length > 0 + ? data.errors[0].error || data.errors[0].message + : data.error || data.message + const errorDetails = data.errors && data.errors.length > 0 && data.errors[0].deployment + ? ` (${data.errors[0].deployment}/${data.errors[0].container})` + : '' + setErrorMessage( + `❌ Failed to update image${errorDetails}: ${errorMsg || 'Unknown error'}` + ) + setTimeout(() => setErrorMessage(null), 10000) + } + } catch (error: any) { + console.error('Error updating image:', error) + setErrorMessage(`❌ Failed to update image: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) + setTimeout(() => setErrorMessage(null), 10000) + } finally { + setIsUpdating(false) + } + } + + const handleUpdateImages = async () => { + console.log('🔄 handleUpdateImages called') + console.log('📦 imageUpdates size:', imageUpdates.size) + console.log('📦 imageUpdates:', Array.from(imageUpdates.entries())) + + if (imageUpdates.size === 0) { + setErrorMessage('No image updates to apply') + return + } + + setIsUpdating(true) + setErrorMessage(null) + + try { + const updates = Array.from(imageUpdates.values()) + console.log('📋 All updates:', updates) + + // If selective mode, only update selected deployments + let updatesToApply = updates + if (updateMode === 'selective') { + console.log('🎯 Selective mode - selectedDeployments:', Array.from(selectedDeployments)) + updatesToApply = updates.filter((update) => { + const key = `${update.namespace}/${update.deployment}` + return selectedDeployments.has(key) + }) + console.log('✅ Filtered updates to apply:', updatesToApply) + } + + if (updatesToApply.length === 0) { + console.log('⚠️ No deployments selected') + setErrorMessage('Please select deployments to update or switch to bulk mode') + setIsUpdating(false) + return + } + + console.log('🚀 Making API call to update images:', updatesToApply) + const response = await fetch(`${apiUrl}/k8s/update-images`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ updates: updatesToApply, source: updateMode === 'selective' ? 'ui-selective' : 'ui-bulk' }), + }) + console.log('📡 API response status:', response.status) + + // Check if response is ok before parsing JSON + if (!response.ok) { + const errorText = await response.text() + let errorData + try { + errorData = JSON.parse(errorText) + } catch { + errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } + } + throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success) { + // All updates succeeded + setErrorMessage(`✅ Successfully updated ${data.updated || updatesToApply.length} container(s)`) + setImageUpdates(new Map()) + setSelectedDeployments(new Set()) + await loadData() + setTimeout(() => setErrorMessage(null), 5000) + } else if (data.updated && data.updated > 0) { + // Partial success - some succeeded, some failed + const errorDetails = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : 'See details above' + setErrorMessage( + `⚠️ Partial update: ${data.updated} succeeded, ${data.failed || 0} failed. ${errorDetails}` + ) + // Clear successful updates from the pending list + if (data.results && Array.isArray(data.results)) { + const newUpdates = new Map(imageUpdates) + data.results.forEach((result: any) => { + const key = `${result.namespace}/${result.deployment}/${result.container}` + newUpdates.delete(key) + }) + setImageUpdates(newUpdates) + } + await loadData() + setTimeout(() => setErrorMessage(null), 8000) + } else { + // All updates failed + const errorMsg = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : data.error || data.message || 'Unknown error' + setErrorMessage(`❌ Failed to update images: ${errorMsg}`) + setTimeout(() => setErrorMessage(null), 10000) + } + } catch (error: any) { + console.error('Error updating images:', error) + setErrorMessage(`❌ Failed to update images: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) + setTimeout(() => setErrorMessage(null), 10000) + } finally { + setIsUpdating(false) + } + } + + const toggleDeploymentSelection = (namespace: string, deploymentName: string) => { + const key = `${namespace}/${deploymentName}` + const newSelected = new Set(selectedDeployments) + + if (newSelected.has(key)) { + newSelected.delete(key) + } else { + newSelected.add(key) + } + + setSelectedDeployments(newSelected) + } + + // Bulk Update Wizard Functions + const handleSelectAllBulkUpdate = () => { + if (bulkUpdateSelections.size === deployments.length) { + setBulkUpdateSelections(new Set()) + } else { + const allKeys = new Set(deployments.map(d => `${d.namespace}/${d.name}`)) + setBulkUpdateSelections(allKeys) + } + } + + const toggleBulkUpdateSelection = (namespace: string, name: string) => { + const key = `${namespace}/${name}` + const newSelected = new Set(bulkUpdateSelections) + + if (newSelected.has(key)) { + newSelected.delete(key) + } else { + newSelected.add(key) + } + + setBulkUpdateSelections(newSelected) + } + + const handleBulkUpdateApply = async () => { + if (bulkUpdateSelections.size === 0) { + setErrorMessage('Please select at least one deployment') + return + } + + if (!bulkUpdateActiveTab || (bulkUpdateActiveTab !== 'main' && bulkUpdateActiveTab !== 'sidecar')) { + setErrorMessage('Please select a container tab (Main or Sidecar)') + return + } + + const containerImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' + const containerTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false + + if (!containerImage.trim()) { + setErrorMessage('Please enter a new ' + (containerTagOnly ? 'tag' : 'image') + ' for ' + bulkUpdateActiveTab + ' containers') + return + } + + setIsUpdating(true) + setErrorMessage(null) + + try { + // Build updates for all selected deployments, but only for containers matching the active tab type + const updates: ImageUpdate[] = [] + const isSidecarTab = bulkUpdateActiveTab === 'sidecar' + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + // Only update containers matching the active tab type (sidecar vs main) + const isContainerSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecarTab === isContainerSidecar) { + // If tag-only mode, apply the tag to the base image + const finalImage = containerTagOnly + ? applyTagOnlyUpdate(container.image, containerImage.trim()) + : containerImage.trim() + + updates.push({ + namespace: deployment.namespace, + deployment: deployment.name, + container: container.name, + image: finalImage, + originalImage: container.image, + }) + } + }) + } + }) + + const response = await fetch(`${apiUrl}/k8s/update-images`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ updates, source: 'ui-bulk' }), + }) + + // Check if response is ok before parsing JSON + if (!response.ok) { + const errorText = await response.text() + let errorData + try { + errorData = JSON.parse(errorText) + } catch { + errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } + } + throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success) { + // All updates succeeded + setErrorMessage(`✅ Successfully updated ${data.updated || updates.length} container(s)`) + setShowBulkUpdateWizard(false) + setBulkUpdateSelections(new Set()) + setBulkUpdateImage('') + setBulkUpdateTagOnly(false) + setBulkUpdateActiveTab('') + setBulkUpdateContainerImages(new Map()) + setBulkUpdateContainerTagOnly(new Map()) + setBulkUpdateStep('select') + await loadData() + setTimeout(() => setErrorMessage(null), 5000) + } else if (data.updated && data.updated > 0) { + // Partial success - some succeeded, some failed + const errorDetails = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : 'See details above' + setErrorMessage( + `⚠️ Partial update: ${data.updated} succeeded, ${data.failed || 0} failed. ${errorDetails}` + ) + setShowBulkUpdateWizard(false) + setBulkUpdateSelections(new Set()) + setBulkUpdateImage('') + setBulkUpdateTagOnly(false) + setBulkUpdateActiveTab('') + setBulkUpdateContainerImages(new Map()) + setBulkUpdateContainerTagOnly(new Map()) + setBulkUpdateStep('select') + await loadData() + setTimeout(() => setErrorMessage(null), 8000) + } else { + // All updates failed + const errorMsg = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : data.error || data.message || 'Unknown error' + setErrorMessage(`❌ Failed to update images: ${errorMsg}`) + setTimeout(() => setErrorMessage(null), 10000) + } + } catch (error: any) { + console.error('Error updating images:', error) + setErrorMessage(`❌ Failed to update images: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) + setTimeout(() => setErrorMessage(null), 10000) + } finally { + setIsUpdating(false) + } + } + + const handleScaleDeployment = async (namespace: string, name: string, replicas: number) => { + setIsActioning(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/deployments/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/scale`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ replicas }), + }) + + const data = await response.json() + + if (data.success) { + setErrorMessage(`✅ ${data.message}`) + setTimeout(() => setErrorMessage(null), 5000) + setActionModal(null) + // Reload deployments to reflect the change + await loadData() + } else { + setErrorMessage(`❌ ${data.error || data.message || 'Failed to scale deployment'}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to scale deployment: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + const handleRestartDeployment = async (namespace: string, name: string) => { + setIsActioning(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/deployments/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/restart`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + + const data = await response.json() + + if (data.success) { + setErrorMessage(`✅ ${data.message}`) + setTimeout(() => setErrorMessage(null), 5000) + setActionModal(null) + // Reload deployments to reflect the change + await loadData() + } else { + setErrorMessage(`❌ ${data.error || data.message || 'Failed to restart deployment'}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to restart deployment: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + const handleDeleteResource = async (resourceType: string, namespace: string, name: string) => { + setIsActioning(true) + setErrorMessage(null) + + try { + // If deleting a deployment, save its spec first for potential restore + if (resourceType === 'deployments') { + const deployment = deployments.find(d => d.name === name && d.namespace === namespace) + if (deployment) { + setDeletedDeployments(prev => [...prev, { + name: deployment.name, + namespace: deployment.namespace, + deletedAt: new Date().toISOString(), + spec: deployment + }]) + } + } + + const response = await fetch(`${apiUrl}/k8s/${resourceType}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }) + + const data = await response.json() + + if (data.success) { + setErrorMessage(`✅ ${data.message}`) + setTimeout(() => setErrorMessage(null), 5000) + setActionModal(null) + // Reload data to reflect the deletion + await loadData() + } else { + setErrorMessage(`❌ ${data.error || data.message || 'Failed to delete resource'}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to delete resource: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + // Handle pod selection toggle + const handlePodSelect = (podKey: string) => { + setSelectedPods(prev => { + const newSet = new Set(prev) + if (newSet.has(podKey)) { + newSet.delete(podKey) + } else { + newSet.add(podKey) + } + return newSet + }) + } + + // Handle select all pods + const handleSelectAllPods = () => { + if (selectedPods.size === pods.length) { + setSelectedPods(new Set()) + } else { + setSelectedPods(new Set(pods.map(p => `${p.namespace}/${p.name}`))) + } + } + + // Handle bulk delete pods + const handleBulkDeletePods = async () => { + if (selectedPods.size === 0) return + + if (!confirm(`Are you sure you want to delete ${selectedPods.size} pod(s)? This action cannot be undone.`)) { + return + } + + setIsDeletingPods(true) + setErrorMessage(null) + + let successCount = 0 + let failCount = 0 + + for (const podKey of selectedPods) { + const [namespace, name] = podKey.split('/') + try { + const response = await fetch(`${apiUrl}/k8s/pods/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + }) + const data = await response.json() + if (data.success) { + successCount++ + } else { + failCount++ + } + } catch { + failCount++ + } + } + + setSelectedPods(new Set()) + setIsDeletingPods(false) + + if (failCount === 0) { + setErrorMessage(`✅ Successfully deleted ${successCount} pod(s)`) + } else { + setErrorMessage(`⚠️ Deleted ${successCount} pod(s), failed to delete ${failCount} pod(s)`) + } + setTimeout(() => setErrorMessage(null), 5000) + await loadData() + } + + // Handle restore deleted deployment + const handleRestoreDeployment = async (deletedDeployment: { name: string; namespace: string; deletedAt: string; spec: any }) => { + if (!confirm(`Are you sure you want to restore deployment "${deletedDeployment.name}" in namespace "${deletedDeployment.namespace}"?`)) { + return + } + + setIsActioning(true) + setErrorMessage(null) + + try { + // Create deployment from saved spec + const response = await fetch(`${apiUrl}/k8s/deployments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + namespace: deletedDeployment.namespace, + name: deletedDeployment.name, + spec: deletedDeployment.spec + }), + }) + + const data = await response.json() + + if (data.success) { + // Remove from deleted deployments list + setDeletedDeployments(prev => prev.filter(d => + !(d.name === deletedDeployment.name && d.namespace === deletedDeployment.namespace && d.deletedAt === deletedDeployment.deletedAt) + )) + setErrorMessage(`✅ Successfully restored deployment "${deletedDeployment.name}"`) + setTimeout(() => setErrorMessage(null), 5000) + await loadData() + } else { + setErrorMessage(`❌ Failed to restore deployment: ${data.error || data.message}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to restore deployment: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + // Handle clear deleted deployment from history + const handleClearDeletedDeployment = (deletedDeployment: { name: string; namespace: string; deletedAt: string; spec: any }) => { + setDeletedDeployments(prev => prev.filter(d => + !(d.name === deletedDeployment.name && d.namespace === deletedDeployment.namespace && d.deletedAt === deletedDeployment.deletedAt) + )) + } + + // Auto-connect on component mount (using ServiceAccount) + useEffect(() => { + if (apiUrl && !isConnected) { + loadNamespaces(true); + } + }, [apiUrl]); + + // Get docker image tag from portal deployment for build version + const portalDeployment = deployments.find(d => d.name.toLowerCase().includes('portal')) + const dockerImageTag = portalDeployment && portalDeployment.containers?.length > 0 + ? portalDeployment.containers[0].image.split(':')[1] || 'latest' + : (deployments.length > 0 && deployments[0].containers?.length > 0 + ? deployments[0].containers[0].image.split(':')[1] || 'latest' + : 'latest') + + // ─── Helper: time ago ─── + const timeAgo = (dateStr: string) => { + if (!dateStr) return '-' + const now = new Date() + const date = new Date(dateStr) + const seconds = Math.floor((now.getTime() - date.getTime()) / 1000) + if (seconds < 0) return 'just now' + if (seconds < 60) return `${seconds}s ago` + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + return `${days}d ago` + } + + // ─── Fetch audit logs for overview ─── + useEffect(() => { + if (activeResourceTab === 'overview' && isConnected && apiUrl) { + fetch(`${apiUrl}/audit/logs?limit=10`) + .then(res => res.json()) + .then(data => { + if (data.success) setAuditLogs(data.logs || []) + }) + .catch(err => console.warn('Failed to fetch audit logs:', err)) + } + }, [activeResourceTab, isConnected, apiUrl]) + + // ─── Fetch pod for Describe modal ─── + useEffect(() => { + if (!describePod || !apiUrl) { + setDescribePodContent(null) + setDescribePodError(null) + return + } + let cancelled = false + setDescribePodLoading(true) + setDescribePodError(null) + setDescribePodContent(null) + fetch(`${apiUrl}/k8s/pods/${encodeURIComponent(describePod.namespace)}/${encodeURIComponent(describePod.name)}`, { + headers: getAuthHeaders(), + }) + .then(res => res.json()) + .then(data => { + if (cancelled) return + if (data.success && data.pod) { + setDescribePodContent(JSON.stringify(data.pod, null, 2)) + } else { + setDescribePodError(data.message || 'Failed to load pod') + } + }) + .catch(err => { + if (!cancelled) setDescribePodError(err.message || 'Failed to fetch pod') + }) + .finally(() => { + if (!cancelled) setDescribePodLoading(false) + }) + return () => { cancelled = true } + }, [describePod, apiUrl]) + + // ─── Global search keyboard shortcut (Cmd+K / Ctrl+K) ─── + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault() + setShowSearchModal(true) + setSearchQuery('') + } + if (e.key === 'Escape' && showSearchModal) { + setShowSearchModal(false) + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [showSearchModal]) + + // ─── Search results computation ─── + const searchResults = useMemo(() => { + if (!searchQuery.trim()) return [] + const q = searchQuery.toLowerCase() + const results: Array<{ type: string; name: string; namespace: string; tabKey: string; resourceType: string }> = [] + deployments.filter(d => d.name.toLowerCase().includes(q)).forEach(d => + results.push({ type: 'Deployment', name: d.name, namespace: d.namespace, tabKey: 'deployments', resourceType: 'deployment' })) + pods.filter(p => p.name.toLowerCase().includes(q)).forEach(p => + results.push({ type: 'Pod', name: p.name, namespace: p.namespace, tabKey: 'pods', resourceType: 'pod' })) + services.filter(s => s.name.toLowerCase().includes(q)).forEach(s => + results.push({ type: 'Service', name: s.name, namespace: s.namespace, tabKey: 'services', resourceType: 'service' })) + configmaps.filter(c => c.name?.toLowerCase().includes(q)).forEach(c => + results.push({ type: 'ConfigMap', name: c.name, namespace: c.namespace, tabKey: 'configmaps', resourceType: 'configmap' })) + secrets.filter(s => s.name?.toLowerCase().includes(q)).forEach(s => + results.push({ type: 'Secret', name: s.name, namespace: s.namespace, tabKey: 'secrets', resourceType: 'secret' })) + cronjobs.filter(c => c.name.toLowerCase().includes(q)).forEach(c => + results.push({ type: 'CronJob', name: c.name, namespace: c.namespace, tabKey: 'cronjobs', resourceType: 'cronjob' })) + jobs.filter(j => j.name.toLowerCase().includes(q)).forEach(j => + results.push({ type: 'Job', name: j.name, namespace: j.namespace, tabKey: 'jobs', resourceType: 'job' })) + ingresses.filter(i => i.name.toLowerCase().includes(q)).forEach(i => + results.push({ type: 'Ingress', name: i.name, namespace: i.namespace, tabKey: 'ingresses', resourceType: 'ingress' })) + pvcs.filter(p => p.name.toLowerCase().includes(q)).forEach(p => + results.push({ type: 'PVC', name: p.name, namespace: p.namespace, tabKey: 'pvcs', resourceType: 'pvc' })) + // Nodes + nodes.filter((n: any) => (n.name || n.metadata?.name || '').toLowerCase().includes(q)).forEach((n: any) => + results.push({ type: 'Node', name: n.name || n.metadata?.name, namespace: '', tabKey: 'overview', resourceType: 'node' })) + // Events + events.filter((e: any) => (e.message || '').toLowerCase().includes(q) || (e.reason || '').toLowerCase().includes(q) || (e.involvedObject?.name || '').toLowerCase().includes(q)).forEach((e: any) => + results.push({ type: 'Event', name: `${e.involvedObject?.kind || ''}/${e.involvedObject?.name || ''} — ${e.reason || ''}`, namespace: e.namespace || '', tabKey: 'events', resourceType: 'event' })) + // Databases from localStorage (connected database services) + try { + const savedDbs = localStorage.getItem('database_connections') + if (savedDbs) { + const dbConns = JSON.parse(savedDbs) as Array<{ name: string; type: string; host?: string; namespace?: string }> + dbConns.filter(db => (db.name || '').toLowerCase().includes(q) || (db.type || '').toLowerCase().includes(q) || (db.host || '').toLowerCase().includes(q)).forEach(db => + results.push({ type: 'Database', name: `${db.name} (${db.type})`, namespace: db.namespace || '', tabKey: 'databases', resourceType: 'database' })) + } + } catch {} + return results.slice(0, 50) + }, [searchQuery, deployments, pods, services, configmaps, secrets, cronjobs, jobs, ingresses, pvcs, nodes, events]) + + // ─── Notifications computation ─── + const notifications = useMemo(() => { + const notifs: Array<{ type: 'error' | 'warning'; message: string; resource: string; namespace: string; timestamp: string }> = [] + // Pods with CrashLoopBackOff or OOMKilled + pods.forEach(pod => { + if (pod.issues && Array.isArray(pod.issues)) { + pod.issues.forEach((issue: any) => { + if (issue.reason === 'CrashLoopBackOff' || issue.reason === 'OOMKilled') { + notifs.push({ type: 'error', message: `${issue.reason}: ${issue.message || pod.name}`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) + } + }) + } + }) + // Pods with any issues (hasIssues flag from API) + pods.forEach(pod => { + if (pod.hasIssues && (!pod.issues || pod.issues.length === 0)) { + notifs.push({ type: 'warning', message: `Pod ${pod.name} has issues (status: ${pod.status})`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) + } + }) + // Pods not in Running/Succeeded state + pods.forEach(pod => { + if (pod.status && pod.status !== 'Running' && pod.status !== 'Succeeded') { + // Avoid duplicate if already caught by issues above + const alreadyNotified = notifs.some(n => n.resource === pod.name && n.namespace === pod.namespace) + if (!alreadyNotified) { + notifs.push({ type: 'warning', message: `Pod ${pod.name} is ${pod.status}`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || pod.creationTimestamp || '' }) + } + } + }) + // Deployments with Stalled / ReplicaFailure / Unavailable / Degraded + deployments.forEach(dep => { + if (dep.deploymentStatus === 'Stalled' || dep.deploymentStatus === 'ReplicaFailure' || dep.deploymentStatus === 'Unavailable' || dep.deploymentStatus === 'Degraded') { + notifs.push({ type: 'error', message: `Deployment ${dep.name} is ${dep.deploymentStatus}`, resource: dep.name, namespace: dep.namespace, timestamp: dep.creationTimestamp }) + } + }) + // Warning events — check both lastTimestamp and eventTime, use 2-hour window + const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000) + events.forEach((event: any) => { + if (event.type === 'Warning') { + const eventTs = event.lastTimestamp || event.eventTime || event.firstTimestamp + if (eventTs && new Date(eventTs) > twoHoursAgo) { + notifs.push({ type: 'warning', message: `${event.reason}: ${(event.message || '').slice(0, 80)}`, resource: `${event.involvedObject?.kind || ''}/${event.involvedObject?.name || ''}`, namespace: event.namespace || '', timestamp: eventTs }) + } + } + }) + // Pods with high restart count (API returns "restarts" field, not "restartCount") + pods.forEach(pod => { + const restarts = (pod as any).restarts ?? (pod as any).restartCount ?? 0 + if (restarts > 5) { + // Avoid duplicate + const alreadyNotified = notifs.some(n => n.resource === pod.name && n.namespace === pod.namespace && n.message.includes('restarts')) + if (!alreadyNotified) { + notifs.push({ type: 'warning', message: `Pod ${pod.name} has ${restarts} restarts`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) + } + } + }) + return notifs.slice(0, 50) + }, [pods, deployments, events]) + + return ( +
+ {/* Left Sidebar - Fixed Dark Theme */} +
+ {/* Logo/Brand Section */} +
+
+
+ +
+ Klarity +
+
+ + {/* Project/Namespace Selector */} +
+
NAMESPACE
+
+ {selectedNamespace || 'Connecting...'} +
+
+ + {/* Navigation Menu */} + + + {/* Bottom Section */} +
+
Build version: {dockerImageTag}
+
+
+ + {/* Main Content Area - Full width with left margin for fixed sidebar */} +
+ {/* Connection Banner - Only when not connected */} + {!isConnected && ( +
+
+
+ + Connecting to Kubernetes cluster via ServiceAccount... +
+
+
+ )} + + {/* Main Content */} + {isConnected && ( +
+ {/* Header Banner - no overflow-hidden so notification dropdown is not clipped */} +
+
+ + + + + + + + +
+
+
+

Welcome to Klarity

+

Experience seamless Kubernetes management with powerful observability and control.

+
+
+ {/* Search Button */} + + {/* Notification Bell */} +
+ + {showNotificationPanel && ( +
+
+

Notifications ({notifications.length})

+ +
+
+ {notifications.length === 0 ? ( +
+ + All clear! No issues detected. +
+ ) : notifications.map((notif, idx) => { + const parts = (notif.resource || '').split('/') + const kind = parts.length > 1 ? parts[0] : 'Pod' + const name = parts.length > 1 ? parts[1] : notif.resource + const tabMap: Record = { Pod: 'pods', Deployment: 'deployments', Service: 'services' } + const typeMap: Record = { Pod: 'pod', Deployment: 'deployment', Service: 'service' } + const tab = tabMap[kind] || 'pods' + const resType = typeMap[kind] || 'pod' + return ( +
{ + setShowNotificationPanel(false) + setActiveResourceTab(tab as any) + if (name) setSelectedResource({ type: resType, namespace: notif.namespace || selectedNamespace, name }) + }} + > +
+ +
+

{notif.message}

+

{notif.resource} · {timeAgo(notif.timestamp)}

+
+
+
+ ) + })} +
+
+ )} +
+
+
+
+ + + + + +
+
+ + {/* Tabs Navigation */} +
+
+ + + {/* Databases tab hidden */} + {enableMongoDBWizard && ( + + )} + {enableAdminerWizard && ( + + )} + {enableImageChangeReport && ( + + )} +
+
+ + {/* Content Area */} +
+ {/* Quick Stats Cards + Resource Summary - hidden on Flow Tracing tab and all DB tabs */} + {activeResourceTab !== 'flow-tracing' && + activeResourceTab !== 'databases' && + activeResourceTab !== 'mongodb-wizard' && + activeResourceTab !== 'adminer-wizard' && ( + <> +
+
+

Cluster Resources

+ + + Connected + +
+
+ + {autoRefresh && ( + + )} + +
+
+ + {/* Compact Resource Summary Cards */} +
+
setActiveResourceTab('deployments')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'deployments' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Deployments +
+
{deployments.length}
+
+
setActiveResourceTab('pods')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'pods' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Pods +
+
{pods.length}
+
+
setActiveResourceTab('services')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'services' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Services +
+
{services.length}
+
+
setActiveResourceTab('cronjobs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'cronjobs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + CronJobs +
+
{cronjobs.length}
+
+
setActiveResourceTab('jobs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'jobs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Jobs +
+
{jobs.length}
+
+
setActiveResourceTab('events')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'events' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Events +
+
{events.length}
+
+
setActiveResourceTab('ingresses')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'ingresses' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Ingresses +
+
{ingresses.length}
+
+
setActiveResourceTab('pvcs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'pvcs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + PVCs +
+
{pvcs.length}
+
+
+ + )} + + {/* ═══ OVERVIEW TAB ═══ */} + {activeResourceTab === 'overview' && ( +
+ {/* Health Summary Cards - clickable, numbers centered */} +
+
setActiveResourceTab('deployments')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Deployments +
+
{deployments.length}
+
+ {deployments.filter(d => d.deploymentStatus === 'Ready').length} healthy + {deployments.filter(d => d.hasIssues).length > 0 && {deployments.filter(d => d.hasIssues).length} issues} +
+
+
setActiveResourceTab('pods')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Pods +
+
+ {pods.filter((p: any) => p.status === 'Running' && p.ready !== false).length} + /{pods.length} +
+
Running
+
+
setActiveResourceTab('services')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Services +
+
{services.length}
+
Active in namespace
+
+
setActiveResourceTab('events')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Warning Events +
+
{events.filter((e: any) => e.type === 'Warning').length}
+
From cluster events
+
+
+ + {/* Deployment Status Distribution Bar */} +
+

Deployment Health

+ {(() => { + const total = deployments.length || 1 + const ready = deployments.filter(d => d.deploymentStatus === 'Ready').length + const progressing = deployments.filter(d => d.deploymentStatus === 'Progressing' || d.deploymentStatus === 'Updating').length + const stalled = deployments.filter(d => d.deploymentStatus === 'Stalled' || d.deploymentStatus === 'Degraded').length + const failed = deployments.filter(d => d.deploymentStatus === 'Unavailable' || d.deploymentStatus === 'ReplicaFailure').length + const other = total - ready - progressing - stalled - failed + return ( +
+
+ {ready > 0 &&
} + {progressing > 0 &&
} + {stalled > 0 &&
} + {failed > 0 &&
} + {other > 0 &&
} +
+
+ Ready: {ready} + {progressing > 0 && Progressing: {progressing}} + {stalled > 0 && Stalled: {stalled}} + {failed > 0 && Failed: {failed}} + {other > 0 && Other: {other}} +
+
+ ) + })()} +
+ + {/* Unhealthy Resources */} + {(() => { + const unhealthyPods = pods.filter((p: any) => p.hasIssues || p.status !== 'Running' || p.ready === false) + if (unhealthyPods.length === 0) return ( +
+ +

All pods are healthy and running.

+
+ ) + return ( +
+
+

⚠ Unhealthy Pods ({unhealthyPods.length})

+
+
+ + + + + + + + + + + + {unhealthyPods.map((pod: any) => ( + { setActiveResourceTab('pods'); setSelectedResource({ type: 'pod', namespace: pod.namespace, name: pod.name }) }}> + + + + + + + ))} + +
PodStatusReasonRestartsAge
{pod.name}{pod.status}{pod.issues?.[0]?.reason || (pod.ready === false ? 'Not Ready' : '-')}{pod.restartCount ?? 0}{timeAgo(pod.creationTimestamp || pod.startTime || '')}
+
+
+ ) + })()} + + {/* Recent Warning Events */} + {(() => { + const warningEvents = events.filter((e: any) => e.type === 'Warning') + if (warningEvents.length === 0) return null + const kindToTab: Record = { Pod: 'pods', Deployment: 'deployments', Service: 'services', ReplicaSet: 'deployments', Node: 'overview' } + const kindToType: Record = { Pod: 'pod', Deployment: 'deployment', Service: 'service', ReplicaSet: 'deployment', Node: null } + return ( +
+
+

Recent Warning Events

+
+
+ + + + + + + + + + + {warningEvents.map((event: any, idx: number) => { + const kind = event.involvedObject?.kind || '' + const name = event.involvedObject?.name || '' + const tab = kindToTab[kind] + const resType = kindToType[kind] + const ns = event.involvedObject?.namespace || event.metadata?.namespace || selectedNamespace || '' + return ( + { + if (tab) setActiveResourceTab(tab as any) + if (resType && name) setSelectedResource({ type: resType, namespace: ns || selectedNamespace, name }) + if (tab === 'overview' && kind === 'Node') setSelectedResource(null) + }} + > + + + + + + ) + })} + +
WhenResourceReasonMessage
{timeAgo(event.lastTimestamp || event.eventTime || '')}{event.involvedObject?.kind}/{event.involvedObject?.name}{event.reason}{event.message}
+
+
+ ) + })()} + + {/* Recent Activity from Audit Logs */} +
+
+

Recent Activity

+
+ {auditLogs.length === 0 ? ( +
No recent activity recorded.
+ ) : ( +
+ {auditLogs.map((log: any, idx: number) => { + const resourceType = (log.resourceType || '').toLowerCase() + const tabMap: Record = { deployments: 'deployments', pods: 'pods', services: 'services', configmaps: 'configmaps', secrets: 'secrets', cronjobs: 'cronjobs', jobs: 'jobs', ingresses: 'ingresses', pvcs: 'pvcs', namespaces: 'overview' } + const typeMap: Record = { deployments: 'deployment', pods: 'pod', services: 'service', configmaps: 'configmap', secrets: 'secret', cronjobs: 'cronjob', jobs: 'job', ingresses: 'ingress', pvcs: 'pvc', namespaces: null } + const tab = tabMap[resourceType] + const resType = typeMap[resourceType] + const ns = log.namespace || selectedNamespace || '' + return ( +
{ + if (tab) setActiveResourceTab(tab as any) + if (resType && log.resourceName) setSelectedResource({ type: resType, namespace: ns, name: log.resourceName }) + }} + > +
+ + {log.user || 'system'} + {log.action} + {log.resourceType}/{log.resourceName} +
+ {timeAgo(log.timestamp)} +
+ ) + })} +
+ )} +
+
+ )} + + {/* Bulk Update Controls - Show for deployments */} + {activeResourceTab === 'deployments' && deployments.length > 0 && ( +
+
+
+ Update Mode: + + +
+ {updateMode === 'bulk' && ( + + )} +
+ {imageUpdates.size > 0 && ( +
+ {imageUpdates.size} image(s) pending update +
+ + +
+
+ )} +
+ )} + + {/* Resource Details Section */} + {/* Detailed Resource View */} +
+ {/* Deployments Tab Content */} + {activeResourceTab === 'deployments' && ( +
+ {/* Deleted Deployments Restore Section - only when deployment delete enabled */} + {enableDeploymentDelete && deletedDeployments.length > 0 && ( +
+ + {showDeletedDeployments && ( +
+
+ Recently Deleted Deployments + (Session only - will be lost on page refresh) +
+
+ {deletedDeployments.map((deleted, idx) => ( +
+
+
{deleted.name}
+
+ Namespace: {deleted.namespace} | Deleted: {new Date(deleted.deletedAt).toLocaleString()} +
+
+
+ + +
+
+ ))} +
+
+ +
+
+ )} +
+ )} + {isLoading ? ( +
+
+ +
+

Loading deployments...

+
+ ) : deployments.length === 0 ? ( +
+
+ +
+

No deployments found

+

{selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}

+
+ ) : ( +
+ + + + {updateMode === 'selective' && ( + + )} + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + {deployments.map((deployment) => + (deployment.containers || []).map((container, idx) => { + const key = `${deployment.namespace}/${deployment.name}/${container.name}` + const update = imageUpdates.get(key) + const isSelected = selectedDeployments.has( + `${deployment.namespace}/${deployment.name}` + ) + + return ( + + {updateMode === 'selective' && idx === 0 && ( + + )} + {selectedNamespace === 'all' && idx === 0 && ( + + )} + {idx === 0 && ( + + )} + + + + {idx === 0 && ( + + )} + + ) + }) + )} + +
+ 0 && + deployments.every((d) => + selectedDeployments.has(`${d.namespace}/${d.name}`) + ) + } + onChange={(e) => { + if (e.target.checked) { + setSelectedDeployments( + new Set( + deployments.map((d) => `${d.namespace}/${d.name}`) + ) + ) + } else { + setSelectedDeployments(new Set()) + } + }} + className="w-4 h-4 rounded border-gray-300 bg-white text-blue-600 focus:ring-blue-500" + /> + + Namespace + + Deployment + + Container + + Current Image + + New Image + + Actions +
+ + toggleDeploymentSelection(deployment.namespace, deployment.name) + } + className="w-4 h-4 rounded border-gray-300 bg-white text-blue-600" + /> + + {deployment.namespace} + +
+ {deployment.name} +
+ {/* Status indicator based on deploymentStatus */} + {deployment.deploymentStatus === 'Ready' ? ( + + + Ready + + ) : deployment.deploymentStatus === 'Progressing' || deployment.deploymentStatus === 'Updating' ? ( + + + {deployment.deploymentStatus} + + ) : deployment.deploymentStatus === 'Stalled' ? ( + + + Stalled + + ) : deployment.deploymentStatus === 'ReplicaFailure' || deployment.deploymentStatus === 'Unavailable' ? ( + + + {deployment.deploymentStatus === 'ReplicaFailure' ? 'Failed' : 'Unavailable'} + + ) : deployment.deploymentStatus === 'Degraded' ? ( + + + Degraded + + ) : deployment.deploymentStatus === 'ScaledToZero' ? ( + + + Scaled to 0 + + ) : deployment.readyReplicas === deployment.replicas && deployment.replicas > 0 ? ( + + + Ready + + ) : deployment.readyReplicas > 0 ? ( + + + Partial + + ) : ( + + + Not Ready + + )} + {deployment.readyReplicas}/{deployment.replicas} +
+
+ {/* Warning triangle for deployments with issues */} + {deployment.hasIssues && ( +
+ + {expandedDeploymentIssues === `${deployment.namespace}-${deployment.name}` && ( +
+
+ + + Deployment Issues ({deployment.issues?.length || 0}) + + +
+
+ {deployment.statusReason && ( +
+
{deployment.statusReason}
+ {deployment.statusMessage && ( +
+ {deployment.statusMessage.length > 200 + ? `${deployment.statusMessage.substring(0, 200)}...` + : deployment.statusMessage} +
+ )} +
+ )} + {deployment.issues && deployment.issues.length > 0 && deployment.issues.map((issue, issueIdx) => ( +
+
+ + {issue.conditionType} + +
+
{issue.reason}
+ {issue.message && ( +
+ {issue.message.length > 150 + ? `${issue.message.substring(0, 150)}...` + : issue.message} +
+ )} +
+
+
+ ))} +
+ {/* Show deployment conditions */} + {deployment.conditions && deployment.conditions.some(c => c.status === 'False') && ( +
+
Conditions:
+
+ {deployment.conditions.filter(c => c.status === 'False').map((cond, condIdx) => ( +
+ + {cond.type}: + {cond.reason || 'Unknown'} +
+ ))} +
+
+ )} +
+ )} +
+ )} +
+ {container.name} + + {container.image} + +
+ + handleImageChange( + deployment.namespace, + deployment.name, + container.name, + e.target.value, + container.image + ) + } + placeholder={container.image} + title={update ? update.image : container.image} + className="flex-1 min-w-0 px-2 py-1.5 bg-white border border-gray-300 rounded text-[11px] font-mono text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + /> + {update && update.image !== container.image && ( + + )} +
+
+
+ + + + {enableDeploymentDelete && ( + + )} + +
+
+
+ )} + + + {imageUpdates.size > 0 && ( +
+
+ + {imageUpdates.size} container image(s) pending update + +
+ + +
+
+
+ )} +
+ )} + + {/* Pods Tab Content */} + {activeResourceTab === 'pods' && ( +
+ {/* Bulk Actions Bar */} + {selectedPods.size > 0 && ( +
+ + {selectedPods.size} pod(s) selected + +
+ + +
+
+ )} + {isLoading ? ( +
+
+ +
+

Loading pods...

+
+ ) : pods.length === 0 ? ( +
+
+ +
+

No pods found

+

{selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}

+
+ ) : ( +
+ + + + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + + + {pods.map((pod) => { + const podKey = `${pod.namespace}/${pod.name}` + return ( + setSelectedResource({ type: 'pod', namespace: pod.namespace, name: pod.name })} + > + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + ) + })} + +
+ 0} + onChange={handleSelectAllPods} + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" + title="Select all pods" + /> + + Namespace + + Pod Name + + Status + + Node + + Pod IP + + Containers + + Restarts + + Actions +
e.stopPropagation()}> + handlePodSelect(podKey)} + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" + /> + + {pod.namespace} + +
+ {pod.name} + {pod.hasIssues && ( +
+ + {expandedPodIssues === `${pod.namespace}-${pod.name}` && ( +
+
+ + + Pod Issues ({pod.issues?.length || 0}) + + +
+
+ {pod.issues && pod.issues.length > 0 ? ( + pod.issues.map((issue, idx) => ( +
+
+ + {issue.type === 'initContainer' ? 'init' : issue.type} + +
+
+ {issue.reason} + {issue.exitCode !== undefined && ( + (exit: {issue.exitCode}) + )} +
+ {issue.containerName && ( +
+ Container: {issue.containerName} +
+ )} + {issue.conditionType && ( +
+ Condition: {issue.conditionType} +
+ )} + {issue.message && ( +
+ {issue.message.length > 150 + ? `${issue.message.substring(0, 150)}...` + : issue.message} +
+ )} +
+
+
+ )) + ) : ( +
+ Pod has issues but no specific details available. + Check container status and conditions. +
+ )} +
+ {/* Show pod conditions if available */} + {pod.conditions && pod.conditions.some(c => c.status === 'False') && ( +
+
Conditions:
+
+ {pod.conditions.filter(c => c.status === 'False').map((cond, idx) => ( +
+ + {cond.type}: + {cond.reason || 'Unknown'} +
+ ))} +
+
+ )} +
+ )} +
+ )} +
+
+ + + {pod.status} + + + + {pod.nodeName || '-'} + + + + {pod.podIP || '-'} + + +
+ {(pod.containers || []).map((container, idx) => ( +
+ + {container.name} + + {container.image.split('/').pop()} + +
+ ))} +
+
+ + {pod.restartCount || 0} + + +
e.stopPropagation()}> + + + + +
+
+
+ )} +
+ )} + + {/* Services Tab Content */} + {activeResourceTab === 'services' && ( +
+ {isLoading ? ( +
+ +

Loading services...

+
+ ) : services.length === 0 ? ( +
+ No services found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + {services.map((service) => ( + + + + + + + + ))} + +
Service NameTypeCluster IPPortsActions
setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {service.name} + setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {service.type} + setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {service.clusterIP || '-'} + setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {(service.ports || []).map((port, idx) => ( +
+ {port.port}/{port.protocol} +
+ ))} +
e.stopPropagation()}> +
+ + {(service.ports && service.ports.length > 0) ? ( + + ) : ( + null + )} +
+
+
+ )} +
+ )} + + {/* ConfigMaps Tab Content */} + {activeResourceTab === 'configmaps' && ( +
+ {isLoading ? ( +
+ +

Loading configmaps...

+
+ ) : configmaps.length === 0 ? ( +
+ No configmaps found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + {configmaps.map((cm) => ( + setSelectedResource({ type: 'configmap', namespace: cm.namespace, name: cm.name })} + > + + + + + ))} + +
ConfigMap NameData KeysCreated
{cm.name} +
+ {Object.keys(cm.data || {}).map((key) => ( + + {key} + + ))} +
+
+ {cm.creationTimestamp ? new Date(cm.creationTimestamp).toLocaleDateString() : '-'} +
+
+ )} +
+ )} + + {/* Secrets Tab Content */} + {activeResourceTab === 'secrets' && ( +
+
+ +
+ {isLoading ? ( +
+ +

Loading secrets...

+
+ ) : secrets.length === 0 ? ( +
+ No secrets found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + {secrets.map((s) => ( + setSelectedResource({ type: 'secret', namespace: s.namespace, name: s.name })} + > + + + + + + ))} + +
Secret NameTypeKeysCreated
{s.name}{s.type || 'Opaque'} +
+ {(s.keys || []).map((key: string) => ( + + {key} + + ))} +
+
+ {s.age ? new Date(s.age).toLocaleDateString() : '-'} +
+
+ )} +
+ )} + + {/* Events Tab Content */} + {activeResourceTab === 'events' && ( +
+ {isLoading ? ( +
+ +

Loading events...

+
+ ) : events.length === 0 ? ( +
+ No events found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + {events.map((event) => ( + setSelectedEvent(event)} + > + + + + + + + + ))} + +
ObjectTypeReasonMessageCountLast Seen
+ {event.involvedObject?.kind}/{event.involvedObject?.name} + + + {event.type} + + {event.reason}{event.message}{event.count || 0} + {event.lastTimestamp ? new Date(event.lastTimestamp).toLocaleString() : '-'} +
+
+ )} +
+ )} + + {/* CronJobs Tab Content */} + {activeResourceTab === 'cronjobs' && ( +
+ {isLoading ? ( +
+ +

Loading cronjobs...

+
+ ) : cronjobs.length === 0 ? ( +
+ No cronjobs found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + + {cronjobs.map((cronjob) => ( + setSelectedResource({ type: 'cronjob', namespace: cronjob.namespace, name: cronjob.name })} + > + + + + + + + + + + ))} + +
NameScheduleSuspendActiveLast ScheduleContainersCreatedActions
{cronjob.name}{cronjob.schedule || '-'} + + {cronjob.suspend ? 'Yes' : 'No'} + + {cronjob.active} + {cronjob.lastScheduleTime ? new Date(cronjob.lastScheduleTime).toLocaleString() : '-'} + +
+ {(cronjob.containers || []).map((container) => ( + + {container.name}: {container.image.split(':')[0]} + + ))} +
+
+ {cronjob.creationTimestamp ? new Date(cronjob.creationTimestamp).toLocaleDateString() : '-'} + + +
+
+ )} +
+ )} + + {/* Jobs Tab Content */} + {activeResourceTab === 'jobs' && ( +
+ {isLoading ? ( +
+ +

Loading jobs...

+
+ ) : jobs.length === 0 ? ( +
+ No jobs found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + + {jobs.map((job) => { + const jobAge = job.startTime ? new Date(job.startTime) : null + const ageString = jobAge ? `${Math.floor((Date.now() - jobAge.getTime()) / (1000 * 60))}m` : '-' + return ( + setSelectedResource({ type: 'job', namespace: job.namespace, name: job.name })} + > + + + + + + + + + + ) + })} + +
NameCompletionsSucceededFailedActiveAgeContainersActions
{job.name}{job.succeeded}/{job.completions} + + {job.succeeded} + + + {job.failed > 0 ? ( + + {job.failed} + + ) : ( + 0 + )} + + {job.active > 0 ? ( + + {job.active} + + ) : ( + 0 + )} + {ageString} +
+ {(job.containers || []).map((container) => ( + + {container.name}: {container.image.split(':')[0]} + + ))} +
+
+ +
+
+ )} +
+ )} +
+ + {/* Flow Tracing Tab Content */} + {activeResourceTab === 'flow-tracing' && ( +
+ +
+ )} + + {/* Databases Tab Content - only when feature enabled */} + {enableDatabase && activeResourceTab === 'databases' && ( +
+ +
+ )} + + {/* MongoDB Compass Tab Content */} + {enableMongoDBWizard && activeResourceTab === 'mongodb-wizard' && ( +
+ +
+ )} + + {/* Adminer Tab Content */} + {enableAdminerWizard && activeResourceTab === 'adminer-wizard' && ( +
+ +
+ )} + + {/* ═══ INGRESSES TAB ═══ */} + {activeResourceTab === 'ingresses' && ( +
+ {isLoading ? ( +
+ +

Loading ingresses...

+
+ ) : ingresses.length === 0 ? ( +
+ No ingresses found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + {ingresses.map((ing) => ( + + + + + + + + + + ))} + +
NameClassHostsPathsBackendTLSAge
{ing.name}{ing.ingressClassName || —} + {ing.rules.map((r, i) =>
{r.host}
)} +
+
+ {ing.rules.flatMap((r, ri) => r.paths.map((p, pi) => ( + {p.path} + )))} +
+
+
+ {ing.rules.flatMap((r, ri) => r.paths.map((p, pi) => ( + p.backend?.serviceName ?
{p.backend.serviceName}:{p.backend.servicePort}
: null + )))} +
+
+ {ing.tls && ing.tls.length > 0 ? ( + 🔒 TLS + ) : ( + — + )} + {ing.creationTimestamp ? timeAgo(ing.creationTimestamp) : '-'}
+
+ )} +
+ )} + + {/* ═══ PVCs TAB ═══ */} + {activeResourceTab === 'pvcs' && ( +
+ {isLoading ? ( +
+ +

Loading persistent volume claims...

+
+ ) : pvcs.length === 0 ? ( +
+ No PVCs found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + {pvcs.map((pvc) => ( + + + + + + + + + + ))} + +
NameStatusStorage ClassCapacityAccess ModesVolumeAge
{pvc.name} + {pvc.status} + {pvc.storageClassName || —}{pvc.capacity || '—'} +
+ {pvc.accessModes.map((mode) => ( + {mode} + ))} +
+
{pvc.volumeName || '—'}{pvc.creationTimestamp ? timeAgo(pvc.creationTimestamp) : '-'}
+
+ )} +
+ )} + + {/* Image change report: per-deployment image change count + which images (from cluster) - only when feature enabled */} + {enableImageChangeReport && activeResourceTab === 'image-change-report' && ( +
+
+

Image change report

+

Per deployment: how many times the image was changed (count) and which images (flow from start to current). Changes made via Selective or Bulk Update in Deployments are reflected here. Download CSV available.

+ {(deployedNamespace || selectedNamespace) && (deployedNamespace || selectedNamespace) !== 'all' ? ( + <> +
+
+ Namespace: + {deployedNamespace || selectedNamespace} +
+ +
+ {imageChangeReportData && ( + <> + {/* Current deployment images from cluster - always shown when we have data */} + {imageChangeReportData.currentDeployments && imageChangeReportData.currentDeployments.length > 0 && ( +
+
+ + Current deployment images in namespace {deployedNamespace || selectedNamespace} (from cluster — change count & flow from ReplicaSet history) + + +
+ + + + + + + + + + + {imageChangeReportData.currentDeployments.map((row, i) => ( + + + + + + + ))} + +
Service (Deployment)Image changesCurrent image listFlow (start → current)
{row.deployment}{row.changeCount ?? 0} +
    + {row.images?.map((img, j) => ( +
  • {img.container ? `${img.container}: ${img.image}` : img.image}
  • + ))} +
+
+ {row.imageFlow && row.imageFlow.length > 0 ? ( +
+ {row.imageFlow.map((rev, idx) => ( + + {idx > 0 && →} + im.image).join(', ')}> + Rev{rev.revision ?? idx + 1} ({rev.createdAt ? new Date(rev.createdAt).toISOString().slice(0, 10) : '-'}): {rev.images?.map((im) => im.image?.split(':').pop() || im.image).join(', ')} + + + ))} +
+ ) : ( + — + )} +
+
+ )} + + {/* Recent Image Changes - Detailed History by Deployment (collapsible) */} + {imageChangeReportData.recentInNamespace && imageChangeReportData.recentInNamespace.length > 0 && (() => { + const recent = imageChangeReportData.recentInNamespace + const byDeployment = recent.reduce((acc: Record, entry) => { + const key = entry.deployment || 'unknown' + if (!acc[key]) acc[key] = [] + acc[key].push(entry) + return acc + }, {}) + const deploymentNames = Object.keys(byDeployment) + const toggleExpanded = (key: string) => { + setExpandedImageHistoryDeployments(prev => { + const next = new Set(prev) + if (next.has(key)) next.delete(key) + else next.add(key) + return next + }) + } + return ( +
+
+ + Recent image changes — by deployment ({recent.length} entries) + + +
+ {deploymentNames.map((depName) => { + const entries = byDeployment[depName] + const count = entries.length + const isExpanded = expandedImageHistoryDeployments.has(depName) + return ( +
+ + {isExpanded && ( +
+ + + + + + + + + + + + + {entries.map((entry, i) => { + const sourceLabel = entry.source === 'ui-single' ? 'UI - Single Edit' : entry.source === 'ui-bulk' ? 'UI - Bulk Update' : entry.source === 'ui-selective' ? 'UI - Selective' : entry.source === 'api' ? 'API / Terminal' : entry.source || 'Unknown' + const sourceBg = entry.source === 'ui-single' ? 'bg-green-100 text-green-800' : entry.source === 'ui-bulk' ? 'bg-purple-100 text-purple-800' : entry.source === 'ui-selective' ? 'bg-blue-100 text-blue-800' : entry.source === 'api' ? 'bg-orange-100 text-orange-800' : 'bg-gray-100 text-gray-700' + return ( + + + + + + + + + ) + })} + +
WhenDeploymentContainerOld Image → New ImageSourceChanged By
+
{entry.at ? new Date(entry.at).toLocaleDateString() : entry.date}
+
{entry.at ? new Date(entry.at).toLocaleTimeString() : ''}
+
{entry.deployment}{entry.container || '—'} +
+ {entry.oldImage ? ( + <> + {entry.oldImage.length > 40 ? '...' + entry.oldImage.slice(-37) : entry.oldImage} + → + {(entry.image || '').length > 40 ? '...' + (entry.image || '').slice(-37) : entry.image} + + ) : ( + {(entry.image || '').length > 50 ? '...' + (entry.image || '').slice(-47) : entry.image} + )} +
+
+ {sourceLabel} + {entry.user || 'anonymous'}
+
+ )} +
+ ) + })} +
+ ) + })()} + + {/* No audit data message */} + {imageChangeReportData.recentInNamespace && imageChangeReportData.recentInNamespace.length === 0 && ( +
+ No image change history found in audit logs. Changes made via the UI will appear here after you update images. +
+ )} + + )} + + ) : ( +
+ Connect to the cluster to see the image change report for your deployed namespace. +
+ )} +
+
+ )} + + {/* Download Images - Auto Excel Download */} + {activeResourceTab === 'download-images' && (() => { + // Auto-trigger Excel download when this tab is selected + const downloadImagesExcel = () => { + // Find max containers in any deployment + const maxContainers = Math.max(...deployments.map(d => d.containers.length), 1) + + // Build header with Container 1, Container 2, Container 3 columns + const header = ['S.No', 'Deployment', 'Namespace'] + for (let i = 1; i <= maxContainers; i++) { + header.push(`Container ${i} Name`, `Container ${i} Image`, `Container ${i} Tag`) + } + + const rows: string[][] = [header] + + let sno = 1 + deployments.forEach(d => { + const row: string[] = [ + String(sno++), + d.name, + d.namespace + ] + + // Add each container in separate columns + for (let i = 0; i < maxContainers; i++) { + if (d.containers[i]) { + const imageParts = d.containers[i].image.split(':') + const imageName = imageParts[0] + const imageTag = imageParts[1] || 'latest' + row.push(d.containers[i].name, imageName, imageTag) + } else { + row.push('', '', '') + } + } + + rows.push(row) + }) + + // Convert to CSV format (Excel compatible) + const csvContent = rows.map(row => + row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',') + ).join('\n') + + // Create and download file + const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }) + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + link.setAttribute('download', `${selectedNamespace}-images-${new Date().toISOString().split('T')[0]}.csv`) + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // Switch back to deployments tab after download + setTimeout(() => setActiveResourceTab('deployments'), 100) + } + + // Trigger download immediately + setTimeout(downloadImagesExcel, 100) + + return ( +
+
+
+ + + +
+

Downloading Images List...

+

+ Exporting {deployments.reduce((acc, d) => acc + d.containers.length, 0)} container images from {selectedNamespace} namespace +

+
+
+ ) + })()} + +
+
+ )} +
+ + {/* Old Pods Table - Removed (now in tabbed pane) */} + {false && isConnected && selectedNamespace && showPods && ( +
+
+

+ Pods ({pods.length}) {selectedNamespace === 'all' ? '- All Namespaces' : `- Namespace: ${selectedNamespace}`} +

+ +
+ + {isLoading ? ( +
+ +

Loading pods...

+
+ ) : pods.length === 0 ? ( +
+ No pods found {selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`} +
+ ) : ( +
+ + + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + + + {pods.map((pod) => ( + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + ))} + +
+ Namespace + + Pod Name + + Status + + Node + + Pod IP + + Containers + + Restarts + + Actions +
+ {pod.namespace} + +
+ {pod.name} + {pod.hasIssues && ( +
+ + {expandedPodIssues === `${pod.namespace}-${pod.name}` && ( +
+
+ + + Pod Issues ({pod.issues?.length || 0}) + + +
+
+ {pod.issues && pod.issues.length > 0 ? ( + pod.issues.map((issue, idx) => ( +
+
+ + {issue.type === 'initContainer' ? 'init' : issue.type} + +
+
+ {issue.reason} + {issue.exitCode !== undefined && ( + (exit: {issue.exitCode}) + )} +
+ {issue.containerName && ( +
+ Container: {issue.containerName} +
+ )} + {issue.conditionType && ( +
+ Condition: {issue.conditionType} +
+ )} + {issue.message && ( +
+ {issue.message.length > 150 + ? `${issue.message.substring(0, 150)}...` + : issue.message} +
+ )} +
+
+
+ )) + ) : ( +
+ Pod has issues but no specific details available. + Check container status and conditions. +
+ )} +
+ {/* Show pod conditions if available */} + {pod.conditions && pod.conditions.some(c => c.status === 'False') && ( +
+
Conditions:
+
+ {pod.conditions.filter(c => c.status === 'False').map((cond, idx) => ( +
+ + {cond.type}: + {cond.reason || 'Unknown'} +
+ ))} +
+
+ )} +
+ )} +
+ )} +
+
+ + {pod.status} + + + {pod.nodeName || '-'} + + {pod.podIP || '-'} + +
+ {(pod.containers || []).map((container, idx) => ( +
+ {container.name}:{' '} + {container.image} + {container.ready !== undefined && ( + + {container.ready ? '✓' : '✗'} + + )} +
+ ))} +
+
+ {pod.restartCount || 0} + +
+ +
+
+
+ )} +
+ )} + + + {/* Services Table with Edit */} + {/* Old Services Table - Removed (now in tabbed pane) */} + {false && isConnected && selectedNamespace && showServices && ( +
+
+

+ Services ({services.length}) {selectedNamespace === 'all' ? '- All Namespaces' : `- Namespace: ${selectedNamespace}`} +

+ +
+ + {isLoading ? ( +
+ +

Loading services...

+
+ ) : services.length === 0 ? ( +
+ No services found {selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`} +
+ ) : ( +
+ + + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + {services.map((service) => ( + + {selectedNamespace === 'all' && ( + + )} + + + + + + + ))} + +
+ Namespace + + Service + + Type + + Cluster IP + + Ports + + Selector +
+ {service.namespace} + + {service.name} + + + {service.type} + + + {service.clusterIP || '-'} + + {(service.ports || []).map((port, idx) => ( +
+ {port.port}:{port.targetPort}/{port.protocol} + {port.name && ` (${port.name})`} +
+ ))} +
+
+ {Object.entries(service.selector).map(([key, value]) => ( + + {key}={value} + + ))} +
+
+
+ )} +
+ )} + + {/* Port Forward Modal (OpenLens-style) */} + {portForwardModal && ( +
+
+
setPortForwardModal(null)} /> +
+
+
+ +

Port forward

+
+ +
+

+ {portForwardModal.service.name} + · + {portForwardModal.service.namespace} +

+ +
+
+ + +
+
+ + setPortForwardModal({ ...portForwardModal, localPort: Number(e.target.value) || portForwardModal.targetPort })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" + /> +
+
+ + + + Open in browser (no kubectl) + + +
+

Or run in terminal

+
+ + kubectl port-forward -n {portForwardModal.service.namespace} svc/{portForwardModal.service.name} {portForwardModal.localPort}:{portForwardModal.targetPort} + + +
+
+
+
+
+ )} + + {/* Resource Detail Modal */} + setSelectedResource(null)} + resourceType={selectedResource?.type || null} + namespace={selectedResource?.namespace || ''} + name={selectedResource?.name || ''} + apiUrl={apiUrl} + onResourceDeleted={loadData} + /> + + {/* Describe Pod Modal */} + {describePod && ( +
+
+
setDescribePod(null)} aria-hidden /> +
+
+

+ Describe Pod: {describePod.name} + ({describePod.namespace}) +

+ +
+
+ {describePodLoading && ( +
+ +
+ )} + {describePodError && ( +

{describePodError}

+ )} + {!describePodLoading && !describePodError && describePodContent && ( +
+                    {describePodContent}
+                  
+ )} +
+
+
+
+ )} + + {/* Create Secret Modal */} + {showCreateSecretModal && ( +
+
+
setShowCreateSecretModal(false)} /> +
+

Create Secret

+

+ Namespace: {selectedNamespace} (key-value pairs are stored as base64) +

+
+ + setCreateSecretName(e.target.value)} + placeholder="my-secret" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-amber-500 focus:border-amber-500" + /> +
+
+ + {createSecretData.map((row, idx) => ( +
+ { + const next = [...createSecretData] + next[idx] = { ...next[idx], key: e.target.value } + setCreateSecretData(next) + }} + placeholder="key" + className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono" + /> + { + const next = [...createSecretData] + next[idx] = { ...next[idx], value: e.target.value } + setCreateSecretData(next) + }} + placeholder="value" + className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono" + /> + +
+ ))} + +
+ {createSecretError && ( +
+ {createSecretError} +
+ )} +
+ + +
+
+
+
+ )} + + {selectedLogPod && ( + setSelectedLogPod(null)} + podName={selectedLogPod.name} + namespace={selectedLogPod.namespace} + container={selectedLogPod.container} + apiUrl={apiUrl} + /> + )} + + {/* Event Detail Modal */} + {selectedEvent && ( +
+
+
+
+

Event Details

+

+ {selectedEvent.involvedObject?.kind}/{selectedEvent.involvedObject?.name} +

+
+ +
+
+
+
+ +
+ + {selectedEvent.type} + +
+
+
+ +

{selectedEvent.reason}

+
+
+ +

{selectedEvent.count || 1}

+
+
+ +

+ {selectedEvent.source?.component || '-'} / {selectedEvent.source?.host || '-'} +

+
+
+ +
+ +

{selectedEvent.message}

+
+ +
+
+ +

+ {selectedEvent.firstTimestamp ? new Date(selectedEvent.firstTimestamp).toLocaleString() : '-'} +

+
+
+ +

+ {selectedEvent.lastTimestamp ? new Date(selectedEvent.lastTimestamp).toLocaleString() : '-'} +

+
+
+ +
+ +
+

Kind: {selectedEvent.involvedObject?.kind}

+

Name: {selectedEvent.involvedObject?.name}

+

Namespace: {selectedEvent.involvedObject?.namespace}

+ {selectedEvent.involvedObject?.uid && ( +

UID: {selectedEvent.involvedObject.uid}

+ )} +
+
+ + {selectedEvent.name && ( +
+ +

{selectedEvent.name}

+
+ )} +
+
+
+ )} + + {/* Bulk Update Image Wizard Modal */} + {showBulkUpdateWizard && ( +
+
+
+
+

Bulk Image Update Wizard

+

+ {bulkUpdateStep === 'select' + ? 'Step 1: Select deployments to update' + : 'Step 2: Review and apply updates'} +

+
+ +
+ +
+ {bulkUpdateStep === 'select' ? ( +
+
+ +
+ {bulkUpdateSelections.size} deployment(s) selected +
+
+ +
+ + + + + + + + + + + {deployments.map((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + const isSelected = bulkUpdateSelections.has(key) + return ( + + + + + + + ) + })} + +
DeploymentContainersCurrent Images
+ toggleBulkUpdateSelection(deployment.namespace, deployment.name)} + className="w-4 h-4" + /> + +
{deployment.name}
+
{deployment.namespace}
+
+
+ {(deployment.containers || []).map((container, idx) => ( +
+ {container.name} +
+ ))} +
+
+
+ {(deployment.containers || []).map((container, idx) => ( +
+ {container.image} +
+ ))} +
+
+
+ + {/* Container Tabs Section - Only show if deployments are selected */} + {bulkUpdateSelections.size > 0 && (() => { + // Group all containers by type (sidecar vs main) + const mainContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + const sidecarContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + const isSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecar) { + sidecarContainers.push({ deployment, container }) + } else { + mainContainers.push({ deployment, container }) + } + }) + } + }) + + // Set initial active tab if not set + if (!bulkUpdateActiveTab) { + if (mainContainers.length > 0) { + setBulkUpdateActiveTab('main') + } else if (sidecarContainers.length > 0) { + setBulkUpdateActiveTab('sidecar') + } + } + + const isActiveSidecar = bulkUpdateActiveTab === 'sidecar' + const activeContainers = isActiveSidecar ? sidecarContainers : mainContainers + const activeImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' + const activeTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false + + return ( +
+ {/* Container Type Tabs */} +
+
+ {mainContainers.length > 0 && ( + + )} + {sidecarContainers.length > 0 && ( + + )} +
+
+ + {/* Selected Container Type's Deployments Table */} + {activeContainers.length > 0 && ( +
+
+ + {isActiveSidecar ? 'Sidecar' : 'Main'} Containers ({activeContainers.length} total) + +
+ + + + + + + + + + + {activeContainers.map((item, idx) => ( + + + + + + + ))} + +
DeploymentNamespaceContainer NameCurrent Image
+
{item.deployment.name}
+
+
{item.deployment.namespace}
+
+
{item.container.name}
+
+
{item.container.image}
+
+
+ )} + + {/* Image Input Section - Different for sidecar vs main containers */} +
+
+ { + const newMap = new Map(bulkUpdateContainerTagOnly) + newMap.set(bulkUpdateActiveTab, e.target.checked) + setBulkUpdateContainerTagOnly(newMap) + }} + className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + +
+
+ + { + const newMap = new Map(bulkUpdateContainerImages) + newMap.set(bulkUpdateActiveTab, e.target.value) + setBulkUpdateContainerImages(newMap) + }} + placeholder={activeTagOnly ? "e.g., 1.12.0-rc5 or latest" : "e.g., nginx:1.25 or myregistry.io/app:v2.0"} + className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm" + /> + {activeTagOnly && ( +

+ Only the tag portion will be updated. Base image will remain unchanged. +

+ )} +
+
+
+ ) + })()} +
+ ) : ( +
+ {(() => { + // Group all containers by type (sidecar vs main) + const mainContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + const sidecarContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + const isSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecar) { + sidecarContainers.push({ deployment, container }) + } else { + mainContainers.push({ deployment, container }) + } + }) + } + }) + + const isActiveSidecar = bulkUpdateActiveTab === 'sidecar' + const activeContainers = isActiveSidecar ? sidecarContainers : mainContainers + const activeImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' + const activeTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false + + return ( + <> + {/* Container Type Tabs */} +
+
+ {mainContainers.length > 0 && ( + + )} + {sidecarContainers.length > 0 && ( + + )} +
+
+ +
+

Update Summary

+
+

• {bulkUpdateSelections.size} deployment(s) selected

+

• Container Type: {isActiveSidecar ? 'Sidecar' : 'Main'}

+

• Containers to update: {activeContainers.length}

+

• New {activeTagOnly ? 'tag' : 'image'}: {activeImage}

+
+
+ +
+
+ + + + + + + + + + + + {activeContainers.map((item, idx) => { + const newImage = activeTagOnly + ? applyTagOnlyUpdate(item.container.image, activeImage.trim()) + : activeImage.trim() + return ( + + + + + + + + ) + })} + +
DeploymentNamespaceContainer NameCurrent ImageNew Image
{item.deployment.name}{item.deployment.namespace}{item.container.name}{item.container.image} + {newImage} +
+
+
+ + ) + })()} +
+ )} +
+ +
+ +
+ {bulkUpdateStep === 'select' ? ( + + ) : ( + + )} +
+
+
+
+ )} + + {/* Image comparison modal */} + {showImageComparison && ( +
+
setShowImageComparison(false)} + /> +
+
+
+

Image update review

+

+ Compare current vs new images before applying updates +

+
+ +
+
+ {imageUpdates.size === 0 ? ( +

No pending image updates.

+ ) : ( + + + + + + + + + + + + {Array.from(imageUpdates.values()).map((update, idx) => ( + + + + + + + + ))} + +
NamespaceDeploymentContainerCurrent ImageNew Image
{update.namespace}{update.deployment}{update.container}{update.originalImage}{update.image}
+ )} +
+
+ + {imageUpdates.size} change(s) ready to apply + +
+ + +
+
+
+
+ )} + + {/* Action Confirmation Modals */} + {actionModal && ( +
+
+
setActionModal(null)}>
+ +
+
+
+

+ {actionModal.type === 'scale' && 'Scale Deployment'} + {actionModal.type === 'restart' && 'Restart Deployment'} + {actionModal.type === 'delete' && 'Delete Resource'} +

+ +
+ + {actionModal.type === 'scale' && ( +
+

+ Scale deployment {actionModal.name} in namespace {actionModal.namespace} +

+
+ + setScaleReplicas(Math.max(0, parseInt(e.target.value) || 0))} + className="w-20 px-3 py-2 border border-gray-300 rounded-md text-center focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + replicas +
+ {actionModal.currentReplicas !== undefined && ( +

+ Current: {actionModal.currentReplicas} replicas +

+ )} +
+ )} + + {actionModal.type === 'restart' && ( +
+

+ Are you sure you want to restart deployment {actionModal.name} in namespace {actionModal.namespace}? +

+

+ This will trigger a rolling restart of all pods in the deployment. +

+
+ )} + + {actionModal.type === 'delete' && ( +
+
+ +

+ Are you sure you want to delete {actionModal.resourceType} {actionModal.name} in namespace {actionModal.namespace}? +

+
+

+ This action cannot be undone! +

+
+ )} + +
+ + +
+
+
+
+
+ )} + {/* ═══ GLOBAL SEARCH MODAL (Cmd+K) ═══ */} + {showSearchModal && ( +
+
setShowSearchModal(false)} /> +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search deployments, pods, services, databases, events..." + className="flex-1 text-sm text-gray-900 placeholder-gray-400 bg-transparent outline-none" + /> + ESC +
+
+ {searchQuery.trim() === '' ? ( +
+ + Type to search across all resource types... +
+ ) : searchResults.length === 0 ? ( +
No results for "{searchQuery}"
+ ) : ( +
+ {searchResults.map((result, idx) => ( + + ))} +
+ )} +
+
+
+ )} +
+ ) +} diff --git a/benchmarks/pairs/agent-vfc/lensops-kubernetesmanagement-fe4910/vuln.tsx b/benchmarks/pairs/agent-vfc/lensops-kubernetesmanagement-fe4910/vuln.tsx new file mode 100644 index 0000000..bdcea25 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/lensops-kubernetesmanagement-fe4910/vuln.tsx @@ -0,0 +1,5840 @@ +// Provenance: amitknigam84/lensops (vuln). +// repo: amitknigam84/lensops +// commit: 9b87ecc3a44a7fe2a5185cca1fe52441647d5748 +// parent: 9b87ecc3a44a7fe2a5185cca1fe52441647d5748 +// commit_url: https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84 +// cve: +// license: MIT +// function: setErrorMessage +// relpath: components/KubernetesManagement.tsx +// provenance: agent +// mechanism: missing_auth_guard + +}: KubernetesManagementProps) { + const { token } = useAuth() + const [kubeconfig, setKubeconfig] = useState('') + + // Helper to get auth headers + const getAuthHeaders = () => { + const headers: HeadersInit = { 'Content-Type': 'application/json' } + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + return headers + } + const [isConnected, setIsConnected] = useState(false) + const [isConnecting, setIsConnecting] = useState(false) + const [isValidating, setIsValidating] = useState(false) + const [isResolving, setIsResolving] = useState(false) + const [validationResult, setValidationResult] = useState(null) + const [resolutionResult, setResolutionResult] = useState<{ + resolved: boolean + fixes: string[] + remainingIssues: string[] + resolvedKubeconfig: string + hasChanges: boolean + } | null>(null) + const [deployments, setDeployments] = useState([]) + const [services, setServices] = useState([]) + const [pods, setPods] = useState([]) + const [nodes, setNodes] = useState([]) + const [configmaps, setConfigmaps] = useState([]) + const [secrets, setSecrets] = useState([]) + const [events, setEvents] = useState([]) + const [cronjobs, setCronjobs] = useState([]) + const [jobs, setJobs] = useState([]) + const [ingresses, setIngresses] = useState([]) + const [pvcs, setPvcs] = useState([]) + const [namespaces, setNamespaces] = useState([]) + const [selectedNamespace, setSelectedNamespace] = useState('') + const [deployedNamespace, setDeployedNamespace] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [selectedDeployments, setSelectedDeployments] = useState>(new Set()) + const [imageUpdates, setImageUpdates] = useState>(new Map()) + const [isUpdating, setIsUpdating] = useState(false) + const [updateMode, setUpdateMode] = useState<'bulk' | 'selective'>('selective') + const [showDeployments, setShowDeployments] = useState(true) + const [showServices, setShowServices] = useState(true) + const [showPods, setShowPods] = useState(true) + const [showNamespaces, setShowNamespaces] = useState(true) + const [expandedPodIssues, setExpandedPodIssues] = useState(null) + const [expandedDeploymentIssues, setExpandedDeploymentIssues] = useState(null) + const [activeResourceTab, setActiveResourceTab] = useState<'overview' | 'deployments' | 'pods' | 'services' | 'configmaps' | 'secrets' | 'events' | 'cronjobs' | 'jobs' | 'ingresses' | 'pvcs' | 'flow-tracing' | 'databases' | 'download-images' | 'image-change-report' | 'mongodb-wizard' | 'adminer-wizard'>('overview') + const [selectedResource, setSelectedResource] = useState<{ + type: 'deployment' | 'service' | 'pod' | 'configmap' | 'secret' | 'cronjob' | 'job' | 'ingress' | 'pvc' | null + namespace: string + name: string + } | null>(null) + const [selectedEvent, setSelectedEvent] = useState(null) + const [actionModal, setActionModal] = useState<{ + type: 'scale' | 'restart' | 'delete' | null + resourceType: string + namespace: string + name: string + currentReplicas?: number + } | null>(null) + const [isActioning, setIsActioning] = useState(false) + const [scaleReplicas, setScaleReplicas] = useState(1) + const [selectedLogPod, setSelectedLogPod] = useState<{ + namespace: string + name: string + container?: string + } | null>(null) + const [describePod, setDescribePod] = useState<{ namespace: string; name: string } | null>(null) + const [describePodContent, setDescribePodContent] = useState(null) + const [describePodLoading, setDescribePodLoading] = useState(false) + const [describePodError, setDescribePodError] = useState(null) + const [showImageComparison, setShowImageComparison] = useState(false) + const [showBulkUpdateWizard, setShowBulkUpdateWizard] = useState(false) + const [bulkUpdateSelections, setBulkUpdateSelections] = useState>(new Set()) + const [bulkUpdateImage, setBulkUpdateImage] = useState('') + const [bulkUpdateStep, setBulkUpdateStep] = useState<'select' | 'preview'>('select') + const [portForwardModal, setPortForwardModal] = useState<{ service: Service; targetPort: number; localPort: number } | null>(null) + const [portForwardCopied, setPortForwardCopied] = useState(false) + const [showCreateSecretModal, setShowCreateSecretModal] = useState(false) + const [createSecretName, setCreateSecretName] = useState('') + const [createSecretData, setCreateSecretData] = useState<{ key: string; value: string }[]>([{ key: '', value: '' }]) + const [createSecretSaving, setCreateSecretSaving] = useState(false) + const [createSecretError, setCreateSecretError] = useState(null) + const [bulkUpdateTagOnly, setBulkUpdateTagOnly] = useState(false) + // Container-wise update state + const [bulkUpdateActiveTab, setBulkUpdateActiveTab] = useState('') + const [bulkUpdateContainerImages, setBulkUpdateContainerImages] = useState>(new Map()) + const [bulkUpdateContainerTagOnly, setBulkUpdateContainerTagOnly] = useState>(new Map()) + const [kubeconfigs, setKubeconfigs] = useState>([]) + const [imageChangeReportDate, setImageChangeReportDate] = useState(() => { + const now = new Date() + return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}` + }) + const [expandedImageHistoryDeployments, setExpandedImageHistoryDeployments] = useState>(new Set()) + const [imageChangeReportData, setImageChangeReportData] = useState<{ + date: string + byDeployment: Array<{ namespace: string; deployment: string; changeCount: number; images?: string[] }> + totalChanges: number + recentInNamespace?: Array<{ date: string; deployment: string; container?: string; image?: string; oldImage?: string; source?: string; user?: string; at: string }> + currentDeployments?: Array<{ + deployment: string + namespace: string + images: Array<{ container: string; image: string }> + lastUpdated?: string | null + changeCount?: number + imageFlow?: Array<{ createdAt: string; images: Array<{ container: string; image: string }>; revision?: string | number }> + }> + } | null>(null) + const [imageChangeReportLoading, setImageChangeReportLoading] = useState(false) + const [showKubeconfigManager, setShowKubeconfigManager] = useState(false) + const [newKubeconfigName, setNewKubeconfigName] = useState('') + const [newKubeconfigContent, setNewKubeconfigContent] = useState('') + const [isSavingKubeconfig, setIsSavingKubeconfig] = useState(false) + // Auto-refresh state + const [autoRefresh, setAutoRefresh] = useState(false) + const [refreshInterval, setRefreshInterval] = useState(10) // seconds + const [lastRefreshed, setLastRefreshed] = useState(null) + + // Overview, Search, Notifications state + const [auditLogs, setAuditLogs] = useState([]) + const [showSearchModal, setShowSearchModal] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [showNotificationPanel, setShowNotificationPanel] = useState(false) + + // Pod selection for bulk delete + const [selectedPods, setSelectedPods] = useState>(new Set()) + const [isDeletingPods, setIsDeletingPods] = useState(false) + + // Deleted deployments history for restore + const [deletedDeployments, setDeletedDeployments] = useState>([]) + const [showDeletedDeployments, setShowDeletedDeployments] = useState(false) + + // Load kubeconfig from manager settings or MongoDB (MongoDB is source of truth) + useEffect(() => { + // Priority: manager's personal kubeconfig > MongoDB active kubeconfig + if (managerKubeconfig) { + setKubeconfig(managerKubeconfig) + } + // MongoDB kubeconfigs will be loaded via loadSavedKubeconfigs + }, [managerKubeconfig]) + + // When a feature is disabled, switch away from its tab + useEffect(() => { + if (activeResourceTab === 'image-change-report' && !enableImageChangeReport) { + setActiveResourceTab('overview') + } + if (activeResourceTab === 'databases' && !enableDatabase) { + setActiveResourceTab('overview') + } + if (activeResourceTab === 'mongodb-wizard' && !enableMongoDBWizard) { + setActiveResourceTab('overview') + } + if (activeResourceTab === 'adminer-wizard' && !enableAdminerWizard) { + setActiveResourceTab('overview') + } + }, [enableImageChangeReport, enableDatabase, enableMongoDBWizard, enableAdminerWizard, activeResourceTab]) + + // Kubeconfig management removed - using ServiceAccount for cluster access + // No need to load saved kubeconfigs anymore + const loadSavedKubeconfigs = async () => { + // This function is kept for compatibility but does nothing + // Kubeconfig management has been removed in favor of ServiceAccount + console.log('ℹ️ Kubeconfig management removed - using ServiceAccount for cluster access') + } + + const handleSaveKubeconfig = async () => { + if (!newKubeconfigName.trim() || !newKubeconfigContent.trim()) { + setErrorMessage('Name and kubeconfig content are required') + return + } + + setIsSavingKubeconfig(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/kubeconfigs`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ + name: newKubeconfigName.trim(), + kubeconfig: newKubeconfigContent.trim(), + isActive: false + }) + }) + + const data = await response.json() + if (data.success) { + await loadSavedKubeconfigs() + setNewKubeconfigName('') + setNewKubeconfigContent('') + setShowKubeconfigManager(false) + setErrorMessage('✅ Kubeconfig saved successfully!') + setTimeout(() => setErrorMessage(null), 3000) + } else { + setErrorMessage(data.error || 'Failed to save kubeconfig') + } + } catch (error: any) { + setErrorMessage('Failed to save kubeconfig: ' + error.message) + } finally { + setIsSavingKubeconfig(false) + } + } + + const handleActivateKubeconfig = async (id: string) => { + setIsConnecting(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/kubeconfigs/${id}/activate`, { + method: 'POST', + headers: getAuthHeaders() + }) + + const data = await response.json() + if (data.success) { + setIsConnected(true) + setKubeconfig(data.kubeconfig.kubeconfig) + await loadSavedKubeconfigs() + setErrorMessage('✅ Successfully connected to cluster. Loading namespaces...') + try { + await loadNamespaces(true) + await loadData() + } catch (err) { + console.error('Error loading namespaces:', err) + } + setTimeout(() => { + setErrorMessage('✅ Successfully connected and loaded namespaces') + setTimeout(() => setErrorMessage(null), 3000) + }, 1000) + } else { + setErrorMessage(data.error || 'Failed to activate kubeconfig') + setIsConnected(false) + } + } catch (error: any) { + setErrorMessage('Failed to activate kubeconfig: ' + error.message) + setIsConnected(false) + } finally { + setIsConnecting(false) + } + } + + const handleDeleteKubeconfig = async (id: string) => { + if (!confirm('Are you sure you want to delete this kubeconfig?')) { + return + } + + try { + const response = await fetch(`${apiUrl}/k8s/kubeconfigs/${id}`, { + method: 'DELETE', + headers: getAuthHeaders() + }) + + const data = await response.json() + if (data.success) { + await loadSavedKubeconfigs() + // If deleted kubeconfig was active, clear connection + const wasActive = kubeconfigs.find(kc => kc.id === id)?.isActive + if (wasActive) { + setIsConnected(false) + setKubeconfig('') + } + } + } catch (error: any) { + setErrorMessage('Failed to delete kubeconfig: ' + error.message) + } + } + + // Clear validation/resolution results when kubeconfig changes + useEffect(() => { + if (validationResult) { + setValidationResult(null) + } + if (resolutionResult) { + setResolutionResult(null) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [kubeconfig]) + + // Set initial active tab when bulk update selections change + useEffect(() => { + if (showBulkUpdateWizard && bulkUpdateSelections.size > 0 && !bulkUpdateActiveTab) { + // Check if there are main containers or sidecar containers + let hasMainContainers = false + let hasSidecarContainers = false + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + const isSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecar) { + hasSidecarContainers = true + } else { + hasMainContainers = true + } + }) + } + }) + + // Set to 'main' if main containers exist, otherwise 'sidecar' if sidecar containers exist + if (hasMainContainers) { + setBulkUpdateActiveTab('main') + } else if (hasSidecarContainers) { + setBulkUpdateActiveTab('sidecar') + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showBulkUpdateWizard, bulkUpdateSelections.size]) + + const handleResolve = async () => { + if (!kubeconfig.trim()) { + setErrorMessage('Please paste or upload a kubeconfig file') + setResolutionResult(null) + return + } + + setIsResolving(true) + setErrorMessage(null) + setResolutionResult(null) + + try { + const response = await fetch(`${apiUrl}/k8s/resolve`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig }), + }) + + const data = await response.json() + + if (data.success) { + setResolutionResult({ + resolved: data.resolved, + fixes: data.fixes || [], + remainingIssues: data.remainingIssues || [], + resolvedKubeconfig: data.resolvedKubeconfig, + hasChanges: data.hasChanges, + }) + + if (data.resolved && data.hasChanges) { + setErrorMessage( + `✅ Resolved ${data.fixes.length} issue(s). Review the changes below and click "Apply Fixes" to update.` + ) + } else if (data.resolved) { + setErrorMessage('✅ No issues found to resolve') + } else { + setErrorMessage('⚠️ Some issues could not be auto-resolved') + } + + // Auto-validate the resolved config + if (data.resolvedKubeconfig) { + const validateResponse = await fetch(`${apiUrl}/k8s/validate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig: data.resolvedKubeconfig }), + }) + + const validateData = await validateResponse.json() + if (validateData.valid) { + setValidationResult({ + valid: true, + info: validateData.info, + }) + } else { + setValidationResult({ + valid: false, + issues: validateData.issues, + suggestions: validateData.suggestions, + info: validateData.info, + }) + } + } + } else { + setErrorMessage(data.message || 'Failed to resolve kubeconfig issues') + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to resolve kubeconfig') + } finally { + setIsResolving(false) + } + } + + const handleApplyFixes = () => { + if (resolutionResult && resolutionResult.resolvedKubeconfig) { + setKubeconfig(resolutionResult.resolvedKubeconfig) + setResolutionResult(null) + setErrorMessage('✅ Fixed kubeconfig applied. You can now validate or connect.') + setTimeout(() => setErrorMessage(null), 5000) + } + } + + const handleValidate = async () => { + if (!kubeconfig.trim()) { + setErrorMessage('Please paste or upload a kubeconfig file') + setValidationResult(null) + return + } + + setIsValidating(true) + setErrorMessage(null) + setValidationResult(null) + + try { + const response = await fetch(`${apiUrl}/k8s/validate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig }), + }) + + const data = await response.json() + + if (data.valid) { + setValidationResult({ + valid: true, + info: data.info, + }) + setErrorMessage('✅ Kubeconfig is valid and ready to connect') + setTimeout(() => setErrorMessage(null), 5000) + } else { + setValidationResult({ + valid: false, + issues: data.issues || [], + suggestions: data.suggestions || [], + info: data.info, + }) + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to validate kubeconfig') + setValidationResult({ + valid: false, + issues: ['Validation request failed'], + suggestions: ['Check your connection to the API server'], + }) + } finally { + setIsValidating(false) + } + } + + const handleConnect = async () => { + if (!kubeconfig.trim()) { + setErrorMessage('Please paste or upload a kubeconfig file') + return + } + + setIsConnecting(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/connect`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ kubeconfig }), + }) + + const data = await response.json() + + if (response.ok && data.success) { + setIsConnected(true) + setValidationResult({ + valid: true, + info: data.info, + }) + + // Save kubeconfig to MongoDB for retention (MongoDB is source of truth) + try { + const saveResponse = await fetch(`${apiUrl}/k8s/kubeconfigs`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ + name: `Kubeconfig ${new Date().toLocaleString()}`, + kubeconfig: kubeconfig, + isActive: true // Set as active since user just connected with it + }) + }) + const saveData = await saveResponse.json() + if (saveData.success) { + console.log('✅ Kubeconfig saved to MongoDB for retention') + // Reload kubeconfigs list to show the newly saved one + await loadSavedKubeconfigs() + // Clear localStorage since MongoDB is now the source of truth + storage.removeLocal(STORAGE_KEYS.KUBECONFIG) + } + } catch (saveError) { + console.error('Failed to save kubeconfig to MongoDB:', saveError) + // Fallback to localStorage only if MongoDB save fails + storage.setLocal(STORAGE_KEYS.KUBECONFIG, kubeconfig) + } + + setErrorMessage('✅ Successfully connected to cluster. Kubeconfig saved to MongoDB. Loading namespaces...') + // Load namespaces immediately after connection (skip connection check since we just connected) + try { + await loadNamespaces(true) + // Always load data after namespaces are loaded (defaults to 'all' to show all resources) + await loadData() + } catch (err) { + console.error('Error loading namespaces after connection:', err) + setErrorMessage('✅ Connected, but failed to load namespaces. Click "Load Namespaces" to retry.') + } + setTimeout(() => { + if (namespaces.length > 0) { + setErrorMessage('✅ Successfully connected and loaded namespaces') + } else { + setErrorMessage('✅ Connected. Click "Load Namespaces" to fetch namespaces.') + } + setTimeout(() => setErrorMessage(null), 5000) + }, 1000) + } else { + // Handle validation errors or connection errors + if (data.issues && data.suggestions) { + setValidationResult({ + valid: data.valid || false, + issues: data.issues, + suggestions: data.suggestions, + info: data.info, + }) + } + setErrorMessage(data.message || 'Failed to connect to cluster') + setIsConnected(false) + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to connect to cluster') + setIsConnected(false) + } finally { + setIsConnecting(false) + } + } + + const loadNamespaces = async (skipConnectionCheck = false) => { + if (!skipConnectionCheck && !isConnected) { + setErrorMessage('Please connect to a Kubernetes cluster first') + return + } + + setIsLoading(true) + setErrorMessage(null) + + try { + console.log('📋 Loading namespaces from:', `${apiUrl}/k8s/namespaces`) + const response = await fetch(`${apiUrl}/k8s/namespaces`) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: 'Unknown error' })) + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + console.log('📋 Namespaces response:', data) + + if (data.success && data.namespaces) { + setNamespaces(data.namespaces) + setIsConnected(true) // ✅ Mark as connected after successfully loading namespaces + + // Always use deployed namespace (from ServiceAccount) + const namespaceToUse = data.podNamespace || (data.namespaces.length > 0 ? data.namespaces[0].name : null); + + if (namespaceToUse) { + setDeployedNamespace(namespaceToUse) + setSelectedNamespace(namespaceToUse) + console.log('📌 Using deployed namespace:', namespaceToUse) + // Automatically load resources for the deployed namespace + console.log('📦 Loading resources from deployed namespace:', namespaceToUse) + setTimeout(() => { + loadData() + }, 100) + } else { + setErrorMessage('⚠️ No namespace found. Please check POD_NAMESPACE configuration.') + } + } else { + setErrorMessage(data.message || data.error || 'Failed to load namespaces') + } + } catch (error: any) { + console.error('❌ Failed to load namespaces:', error) + setErrorMessage(error.message || 'Failed to load namespaces. Please check your connection and try again.') + } finally { + setIsLoading(false) + } + } + + const loadNodes = async () => { + if (!isConnected) return + try { + const response = await fetch(`${apiUrl}/k8s/nodes`) + const data = await response.json() + if (response.ok && data?.nodes) { + setNodes(data.nodes) + } else { + console.warn('⚠️ Failed to load nodes:', data?.message || data?.error) + setNodes([]) + } + } catch (error: any) { + console.error('❌ Failed to load nodes:', error) + setNodes([]) + } + } + + const loadImageChangeReport = async () => { + if (!apiUrl) return + const namespaceToUse = deployedNamespace || selectedNamespace + if (!namespaceToUse || namespaceToUse === 'all') { + setErrorMessage('No namespace available. Connect to the cluster so the report is scoped to your deployed namespace.') + return + } + setImageChangeReportLoading(true) + setImageChangeReportData(null) + setErrorMessage(null) + try { + const params = new URLSearchParams({ date: imageChangeReportDate, namespace: namespaceToUse }) + const response = await fetch(`${apiUrl}/k8s/image-change-report?${params}`) + const data = await response.json() + if (response.ok && data.success) { + setImageChangeReportData({ + date: data.date, + byDeployment: data.byDeployment || [], + totalChanges: data.totalChanges ?? 0, + recentInNamespace: data.recentInNamespace || [], + currentDeployments: data.currentDeployments || [], + }) + } else { + setErrorMessage(data.message || 'Failed to load image change report') + } + } catch (err: any) { + setErrorMessage(err.message || 'Failed to load image change report') + } finally { + setImageChangeReportLoading(false) + } + } + + const loadData = async () => { + if (!isConnected) { + console.log('⚠️ Not connected, skipping loadData') + return + } + + // Always use deployed namespace (from ServiceAccount) + const namespaceToUse = deployedNamespace || selectedNamespace + if (!namespaceToUse || namespaceToUse === 'all') { + console.log('⚠️ No namespace available, waiting for namespace detection...') + // If we have apiUrl but no namespace yet, try to load namespaces + if (apiUrl && !deployedNamespace) { + loadNamespaces(true) + } + // Don't clear existing data if we're just waiting for namespace + // This prevents "no resources" flash when namespace is temporarily unavailable + return + } + + setIsLoading(true) + setErrorMessage(null) + + // Don't clear data here - let the API responses determine what to show + // This prevents "no resources" flash during refresh + + try { + // Always use deployed namespace + const namespaceParam = namespaceToUse + console.log('📦 Loading resources from deployed namespace:', namespaceParam) + + const [deploymentsRes, servicesRes, podsRes, configmapsRes, secretsRes, eventsRes, cronjobsRes, jobsRes, ingressesRes, pvcsRes] = await Promise.all([ + fetch(`${apiUrl}/k8s/deployments?namespace=${namespaceParam}`), + fetch(`${apiUrl}/k8s/services?namespace=${namespaceParam}`), + fetch(`${apiUrl}/k8s/pods?namespace=${namespaceParam}`), + fetch(`${apiUrl}/k8s/configmaps?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ ConfigMaps fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, configmaps: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/secrets?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Secrets fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, secrets: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/events?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Events fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, events: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/cronjobs?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ CronJobs fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, cronjobs: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/jobs?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Jobs fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, jobs: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/ingresses?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ Ingresses fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, ingresses: [], error: err.message }) }; + }), + fetch(`${apiUrl}/k8s/pvcs?namespace=${namespaceParam}`).catch((err) => { + console.warn('⚠️ PVCs fetch failed:', err); + return { ok: false, status: 500, json: async () => ({ success: false, pvcs: [], error: err.message }) }; + }), + ]) + + const deploymentsData = await deploymentsRes.json() + const servicesData = await servicesRes.json() + const podsData = await podsRes.json() + + console.log('📦 Deployments response:', deploymentsData) + console.log('📦 Services response:', servicesData) + console.log('📦 Pods response:', podsData) + + if (deploymentsData.success) { + console.log('✅ Setting deployments:', deploymentsData.deployments?.length || 0) + setDeployments(deploymentsData.deployments || []) + } else { + console.error('❌ Deployments API error:', deploymentsData.error || deploymentsData.message) + // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues + // This prevents "no resources" flash during refresh when there are transient errors + if (deploymentsRes.status >= 400 && deploymentsRes.status < 500) { + setDeployments([]) + } else { + console.warn('⚠️ Keeping existing deployments due to server/network error') + } + } + + if (servicesData.success) { + console.log('✅ Setting services:', servicesData.services?.length || 0) + setServices(servicesData.services || []) + } else { + console.error('❌ Services API error:', servicesData.error || servicesData.message) + // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues + if (servicesRes.status >= 400 && servicesRes.status < 500) { + setServices([]) + } else { + console.warn('⚠️ Keeping existing services due to server/network error') + } + } + + if (podsData.success) { + const podsArray = podsData.pods || [] + + // Remove duplicates based on namespace + name + const uniquePods = podsArray.reduce((acc: Map, pod: Pod) => { + const key = `${pod.namespace}/${pod.name}` + if (!acc.has(key)) { + acc.set(key, pod) + } else { + console.warn(`⚠️ Duplicate pod found: ${key}`) + } + return acc + }, new Map()) + + const deduplicatedPods: Pod[] = Array.from(uniquePods.values()) + + // Filter by namespace if a specific namespace is selected (not 'all') + const filteredPods: Pod[] = selectedNamespace && selectedNamespace !== 'all' + ? deduplicatedPods.filter((pod: Pod) => pod.namespace === selectedNamespace) + : deduplicatedPods + + console.log('✅ Setting pods:', { + original: podsArray.length, + afterDedup: deduplicatedPods.length, + afterFilter: filteredPods.length, + namespace: namespaceParam, + selectedNamespace: selectedNamespace + }) + + if (podsArray.length !== filteredPods.length) { + console.warn(`⚠️ Pod count mismatch: API returned ${podsArray.length}, displaying ${filteredPods.length}`) + } + + setPods(filteredPods) + } else { + console.error('❌ Pods API error:', podsData.error || podsData.message) + // Only clear data on client errors (4xx), keep existing data on server errors (5xx) or network issues + if (podsRes.status >= 400 && podsRes.status < 500) { + setPods([]) + } else { + console.warn('⚠️ Keeping existing pods due to server/network error') + } + } + + const configmapsData = await configmapsRes.json() + if (configmapsData.success) { + console.log('✅ Setting configmaps:', configmapsData.configmaps?.length || 0) + setConfigmaps(configmapsData.configmaps || []) + } else { + console.error('❌ ConfigMaps API error:', configmapsData.error || configmapsData.message) + setConfigmaps([]) + } + + const secretsData = await secretsRes.json() + if (secretsData.success) { + setSecrets(secretsData.secrets || []) + } else { + setSecrets([]) + } + + const eventsData = await eventsRes.json() + if (eventsData.success) { + console.log('✅ Setting events:', eventsData.events?.length || 0) + setEvents(eventsData.events || []) + } else { + console.error('❌ Events API error:', eventsData.error || eventsData.message) + setEvents([]) + } + + // Handle CronJobs + try { + const cronjobsData = await cronjobsRes.json() + if (cronjobsData.success) { + console.log('✅ Setting cronjobs:', cronjobsData.cronjobs?.length || 0) + setCronjobs(cronjobsData.cronjobs || []) + } else { + console.error('❌ CronJobs API error:', cronjobsData.error || cronjobsData.message, cronjobsData.details) + // Don't show error if it's just that there are no cronjobs (404 or empty list) + if (cronjobsRes.status !== 404 && !cronjobsData.message?.includes('not found')) { + console.warn('⚠️ CronJobs fetch warning:', cronjobsData.message) + } + setCronjobs([]) + } + } catch (cronjobsError: any) { + console.error('❌ Failed to parse CronJobs response:', cronjobsError) + setCronjobs([]) + } + + // Handle Jobs + try { + const jobsData = await jobsRes.json() + if (jobsData.success) { + console.log('✅ Setting jobs:', jobsData.jobs?.length || 0) + setJobs(jobsData.jobs || []) + } else { + console.error('❌ Jobs API error:', jobsData.error || jobsData.message, jobsData.details) + // Don't show error if it's just that there are no jobs (404 or empty list) + if (jobsRes.status !== 404 && !jobsData.message?.includes('not found')) { + console.warn('⚠️ Jobs fetch warning:', jobsData.message) + } + setJobs([]) + } + } catch (jobsError: any) { + console.error('❌ Failed to parse Jobs response:', jobsError) + setJobs([]) + } + + // Handle Ingresses + try { + const ingressesData = await ingressesRes.json() + if (ingressesData.success) { + setIngresses(ingressesData.ingresses || []) + } else { + setIngresses([]) + } + } catch (ingressesError: any) { + console.error('❌ Failed to parse Ingresses response:', ingressesError) + setIngresses([]) + } + + // Handle PVCs + try { + const pvcsData = await pvcsRes.json() + if (pvcsData.success) { + setPvcs(pvcsData.pvcs || []) + } else { + setPvcs([]) + } + } catch (pvcsError: any) { + console.error('❌ Failed to parse PVCs response:', pvcsError) + setPvcs([]) + } + + // Check for HTTP errors + if (!deploymentsRes.ok) { + console.error('❌ Deployments HTTP error:', deploymentsRes.status, deploymentsRes.statusText) + setErrorMessage(`Failed to fetch deployments: ${deploymentsRes.status} ${deploymentsRes.statusText}`) + } + if (!servicesRes.ok) { + console.error('❌ Services HTTP error:', servicesRes.status, servicesRes.statusText) + setErrorMessage(`Failed to fetch services: ${servicesRes.status} ${servicesRes.statusText}`) + } + if (!podsRes.ok) { + console.error('❌ Pods HTTP error:', podsRes.status, podsRes.statusText) + setErrorMessage(`Failed to fetch pods: ${podsRes.status} ${podsRes.statusText}`) + } + } catch (error: any) { + setErrorMessage(error.message || 'Failed to load cluster data') + } finally { + setIsLoading(false) + setLastRefreshed(new Date()) + } + } + + + // Automatically load namespaces and resources on mount when apiUrl is available + useEffect(() => { + if (apiUrl && !isConnected && !isConnecting) { + console.log('🚀 Auto-connecting to Kubernetes cluster via ServiceAccount...') + loadNamespaces(true) // Skip connection check since we're auto-connecting + } + }, [apiUrl]) // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (isConnected && selectedNamespace) { + console.log('🔄 Namespace changed to:', selectedNamespace, '- Loading resources...') + // Clear previous data when namespace changes + setDeployments([]) + setPods([]) + setNodes([]) + setServices([]) + setConfigmaps([]) + setEvents([]) + setCronjobs([]) + setJobs([]) + // Load new data for the selected namespace + loadData() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedNamespace, isConnected]) + + useEffect(() => { + if (isConnected) { + loadNodes() + } else { + setNodes([]) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected]) + + // Auto-refresh effect + useEffect(() => { + if (!autoRefresh || !isConnected) return + + const interval = setInterval(async () => { + console.log('🔄 Auto-refreshing data...') + const currentTab = activeResourceTab + await loadData() + setActiveResourceTab(currentTab) + setLastRefreshed(new Date()) + }, refreshInterval * 1000) + + return () => clearInterval(interval) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoRefresh, refreshInterval, isConnected, deployedNamespace, selectedNamespace]) + + const handleImageChange = ( + namespace: string, + deploymentName: string, + containerName: string, + newImage: string, + originalImage: string + ) => { + const key = `${namespace}/${deploymentName}/${containerName}` + const newUpdates = new Map(imageUpdates) + + if (newImage === originalImage) { + newUpdates.delete(key) + } else { + newUpdates.set(key, { + namespace: namespace, + deployment: deploymentName, + container: containerName, + image: newImage, + originalImage, + }) + } + + setImageUpdates(newUpdates) + } + + // Helper function to extract base image (without tag) + const getBaseImage = (image: string): string => { + // Remove tag if present (format: image:tag) + if (image.includes(':')) { + // Check if it's a port number (registry:port/image:tag) + const parts = image.split(':') + if (parts.length > 2) { + // Has port, get everything before the last colon + const lastColonIndex = image.lastIndexOf(':') + return image.substring(0, lastColonIndex) + } else { + // Simple format: image:tag + return parts[0] + } + } + // Remove digest if present (format: image@sha256:...) + if (image.includes('@')) { + return image.split('@')[0] + } + return image + } + + // Helper function to apply tag-only update + const applyTagOnlyUpdate = (originalImage: string, newTag: string): string => { + const baseImage = getBaseImage(originalImage) + return `${baseImage}:${newTag}` + } + + const handleBulkImageUpdate = (newImage: string) => { + const newUpdates = new Map() + + deployments.forEach((deployment) => { + deployment.containers.forEach((container) => { + const key = `${selectedNamespace}/${deployment.name}/${container.name}` + newUpdates.set(key, { + namespace: selectedNamespace, + deployment: deployment.name, + container: container.name, + image: newImage, + originalImage: container.image, + }) + }) + }) + + setImageUpdates(newUpdates) + } + + const handleSaveSingleImage = async (namespace: string, deployment: string, container: string, newImage: string, originalImage: string) => { + if (newImage === originalImage) { + setErrorMessage('Image unchanged') + setTimeout(() => setErrorMessage(null), 2000) + return + } + + if (!newImage || newImage.trim() === '') { + setErrorMessage('Please enter a valid image name') + setTimeout(() => setErrorMessage(null), 2000) + return + } + + // Validate required fields + const trimmedNamespace = namespace?.trim() + const trimmedDeployment = deployment?.trim() + const trimmedContainer = container?.trim() + const trimmedImage = newImage.trim() + + if (!trimmedNamespace || !trimmedDeployment || !trimmedContainer) { + setErrorMessage('Missing required fields: namespace, deployment, or container name') + setTimeout(() => setErrorMessage(null), 3000) + return + } + + // Ensure deployment name doesn't contain namespace (some APIs return it as namespace/name) + let deploymentName = trimmedDeployment + if (trimmedDeployment.includes('/')) { + const parts = trimmedDeployment.split('/').filter(p => p && p.trim() !== '') + deploymentName = parts.length > 0 ? parts[parts.length - 1] : trimmedDeployment + console.log('Extracted deployment name:', { original: trimmedDeployment, extracted: deploymentName, parts }) + } + + // Final validation after extraction + if (!deploymentName || deploymentName.trim() === '') { + setErrorMessage('Invalid deployment name after processing') + setTimeout(() => setErrorMessage(null), 3000) + return + } + + setIsUpdating(true) + setErrorMessage(null) + + try { + const updatePayload = { + namespace: String(trimmedNamespace), + deployment: String(deploymentName), + container: String(trimmedContainer), + image: String(trimmedImage) + } + + console.log('Saving image update:', { + namespace: trimmedNamespace, + deployment: deploymentName, + container: trimmedContainer, + image: trimmedImage, + originalDeployment: deployment, + payload: updatePayload + }) + + const response = await fetch(`${apiUrl}/k8s/update-images`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + updates: [updatePayload], + source: 'ui-single' + }), + }) + + // Check if response is ok before parsing JSON + if (!response.ok) { + const errorText = await response.text() + let errorData + try { + errorData = JSON.parse(errorText) + } catch { + errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } + } + throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success || (data.updated && data.updated > 0)) { + setErrorMessage(`✅ Successfully updated ${container} image in ${deployment}`) + // Remove from pending updates + const key = `${namespace}/${deployment}/${container}` + const newUpdates = new Map(imageUpdates) + newUpdates.delete(key) + setImageUpdates(newUpdates) + await loadData() + setTimeout(() => setErrorMessage(null), 5000) + } else { + // Check if there are errors in the response + const errorMsg = data.errors && data.errors.length > 0 + ? data.errors[0].error || data.errors[0].message + : data.error || data.message + const errorDetails = data.errors && data.errors.length > 0 && data.errors[0].deployment + ? ` (${data.errors[0].deployment}/${data.errors[0].container})` + : '' + setErrorMessage( + `❌ Failed to update image${errorDetails}: ${errorMsg || 'Unknown error'}` + ) + setTimeout(() => setErrorMessage(null), 10000) + } + } catch (error: any) { + console.error('Error updating image:', error) + setErrorMessage(`❌ Failed to update image: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) + setTimeout(() => setErrorMessage(null), 10000) + } finally { + setIsUpdating(false) + } + } + + const handleUpdateImages = async () => { + console.log('🔄 handleUpdateImages called') + console.log('📦 imageUpdates size:', imageUpdates.size) + console.log('📦 imageUpdates:', Array.from(imageUpdates.entries())) + + if (imageUpdates.size === 0) { + setErrorMessage('No image updates to apply') + return + } + + setIsUpdating(true) + setErrorMessage(null) + + try { + const updates = Array.from(imageUpdates.values()) + console.log('📋 All updates:', updates) + + // If selective mode, only update selected deployments + let updatesToApply = updates + if (updateMode === 'selective') { + console.log('🎯 Selective mode - selectedDeployments:', Array.from(selectedDeployments)) + updatesToApply = updates.filter((update) => { + const key = `${update.namespace}/${update.deployment}` + return selectedDeployments.has(key) + }) + console.log('✅ Filtered updates to apply:', updatesToApply) + } + + if (updatesToApply.length === 0) { + console.log('⚠️ No deployments selected') + setErrorMessage('Please select deployments to update or switch to bulk mode') + setIsUpdating(false) + return + } + + console.log('🚀 Making API call to update images:', updatesToApply) + const response = await fetch(`${apiUrl}/k8s/update-images`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ updates: updatesToApply, source: updateMode === 'selective' ? 'ui-selective' : 'ui-bulk' }), + }) + console.log('📡 API response status:', response.status) + + // Check if response is ok before parsing JSON + if (!response.ok) { + const errorText = await response.text() + let errorData + try { + errorData = JSON.parse(errorText) + } catch { + errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } + } + throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success) { + // All updates succeeded + setErrorMessage(`✅ Successfully updated ${data.updated || updatesToApply.length} container(s)`) + setImageUpdates(new Map()) + setSelectedDeployments(new Set()) + await loadData() + setTimeout(() => setErrorMessage(null), 5000) + } else if (data.updated && data.updated > 0) { + // Partial success - some succeeded, some failed + const errorDetails = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : 'See details above' + setErrorMessage( + `⚠️ Partial update: ${data.updated} succeeded, ${data.failed || 0} failed. ${errorDetails}` + ) + // Clear successful updates from the pending list + if (data.results && Array.isArray(data.results)) { + const newUpdates = new Map(imageUpdates) + data.results.forEach((result: any) => { + const key = `${result.namespace}/${result.deployment}/${result.container}` + newUpdates.delete(key) + }) + setImageUpdates(newUpdates) + } + await loadData() + setTimeout(() => setErrorMessage(null), 8000) + } else { + // All updates failed + const errorMsg = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : data.error || data.message || 'Unknown error' + setErrorMessage(`❌ Failed to update images: ${errorMsg}`) + setTimeout(() => setErrorMessage(null), 10000) + } + } catch (error: any) { + console.error('Error updating images:', error) + setErrorMessage(`❌ Failed to update images: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) + setTimeout(() => setErrorMessage(null), 10000) + } finally { + setIsUpdating(false) + } + } + + const toggleDeploymentSelection = (namespace: string, deploymentName: string) => { + const key = `${namespace}/${deploymentName}` + const newSelected = new Set(selectedDeployments) + + if (newSelected.has(key)) { + newSelected.delete(key) + } else { + newSelected.add(key) + } + + setSelectedDeployments(newSelected) + } + + // Bulk Update Wizard Functions + const handleSelectAllBulkUpdate = () => { + if (bulkUpdateSelections.size === deployments.length) { + setBulkUpdateSelections(new Set()) + } else { + const allKeys = new Set(deployments.map(d => `${d.namespace}/${d.name}`)) + setBulkUpdateSelections(allKeys) + } + } + + const toggleBulkUpdateSelection = (namespace: string, name: string) => { + const key = `${namespace}/${name}` + const newSelected = new Set(bulkUpdateSelections) + + if (newSelected.has(key)) { + newSelected.delete(key) + } else { + newSelected.add(key) + } + + setBulkUpdateSelections(newSelected) + } + + const handleBulkUpdateApply = async () => { + if (bulkUpdateSelections.size === 0) { + setErrorMessage('Please select at least one deployment') + return + } + + if (!bulkUpdateActiveTab || (bulkUpdateActiveTab !== 'main' && bulkUpdateActiveTab !== 'sidecar')) { + setErrorMessage('Please select a container tab (Main or Sidecar)') + return + } + + const containerImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' + const containerTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false + + if (!containerImage.trim()) { + setErrorMessage('Please enter a new ' + (containerTagOnly ? 'tag' : 'image') + ' for ' + bulkUpdateActiveTab + ' containers') + return + } + + setIsUpdating(true) + setErrorMessage(null) + + try { + // Build updates for all selected deployments, but only for containers matching the active tab type + const updates: ImageUpdate[] = [] + const isSidecarTab = bulkUpdateActiveTab === 'sidecar' + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + // Only update containers matching the active tab type (sidecar vs main) + const isContainerSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecarTab === isContainerSidecar) { + // If tag-only mode, apply the tag to the base image + const finalImage = containerTagOnly + ? applyTagOnlyUpdate(container.image, containerImage.trim()) + : containerImage.trim() + + updates.push({ + namespace: deployment.namespace, + deployment: deployment.name, + container: container.name, + image: finalImage, + originalImage: container.image, + }) + } + }) + } + }) + + const response = await fetch(`${apiUrl}/k8s/update-images`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ updates, source: 'ui-bulk' }), + }) + + // Check if response is ok before parsing JSON + if (!response.ok) { + const errorText = await response.text() + let errorData + try { + errorData = JSON.parse(errorText) + } catch { + errorData = { message: errorText || `HTTP ${response.status}: ${response.statusText}` } + } + throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + if (data.success) { + // All updates succeeded + setErrorMessage(`✅ Successfully updated ${data.updated || updates.length} container(s)`) + setShowBulkUpdateWizard(false) + setBulkUpdateSelections(new Set()) + setBulkUpdateImage('') + setBulkUpdateTagOnly(false) + setBulkUpdateActiveTab('') + setBulkUpdateContainerImages(new Map()) + setBulkUpdateContainerTagOnly(new Map()) + setBulkUpdateStep('select') + await loadData() + setTimeout(() => setErrorMessage(null), 5000) + } else if (data.updated && data.updated > 0) { + // Partial success - some succeeded, some failed + const errorDetails = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : 'See details above' + setErrorMessage( + `⚠️ Partial update: ${data.updated} succeeded, ${data.failed || 0} failed. ${errorDetails}` + ) + setShowBulkUpdateWizard(false) + setBulkUpdateSelections(new Set()) + setBulkUpdateImage('') + setBulkUpdateTagOnly(false) + setBulkUpdateActiveTab('') + setBulkUpdateContainerImages(new Map()) + setBulkUpdateContainerTagOnly(new Map()) + setBulkUpdateStep('select') + await loadData() + setTimeout(() => setErrorMessage(null), 8000) + } else { + // All updates failed + const errorMsg = data.errors && data.errors.length > 0 + ? data.errors.map((e: any) => `${e.deployment}/${e.container}: ${e.error}`).join('; ') + : data.error || data.message || 'Unknown error' + setErrorMessage(`❌ Failed to update images: ${errorMsg}`) + setTimeout(() => setErrorMessage(null), 10000) + } + } catch (error: any) { + console.error('Error updating images:', error) + setErrorMessage(`❌ Failed to update images: ${error.message || 'Network error. Please check if the API server is running and you are connected to the cluster.'}`) + setTimeout(() => setErrorMessage(null), 10000) + } finally { + setIsUpdating(false) + } + } + + const handleScaleDeployment = async (namespace: string, name: string, replicas: number) => { + setIsActioning(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/deployments/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/scale`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ replicas }), + }) + + const data = await response.json() + + if (data.success) { + setErrorMessage(`✅ ${data.message}`) + setTimeout(() => setErrorMessage(null), 5000) + setActionModal(null) + // Reload deployments to reflect the change + await loadData() + } else { + setErrorMessage(`❌ ${data.error || data.message || 'Failed to scale deployment'}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to scale deployment: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + const handleRestartDeployment = async (namespace: string, name: string) => { + setIsActioning(true) + setErrorMessage(null) + + try { + const response = await fetch(`${apiUrl}/k8s/deployments/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/restart`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + + const data = await response.json() + + if (data.success) { + setErrorMessage(`✅ ${data.message}`) + setTimeout(() => setErrorMessage(null), 5000) + setActionModal(null) + // Reload deployments to reflect the change + await loadData() + } else { + setErrorMessage(`❌ ${data.error || data.message || 'Failed to restart deployment'}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to restart deployment: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + const handleDeleteResource = async (resourceType: string, namespace: string, name: string) => { + setIsActioning(true) + setErrorMessage(null) + + try { + // If deleting a deployment, save its spec first for potential restore + if (resourceType === 'deployments') { + const deployment = deployments.find(d => d.name === name && d.namespace === namespace) + if (deployment) { + setDeletedDeployments(prev => [...prev, { + name: deployment.name, + namespace: deployment.namespace, + deletedAt: new Date().toISOString(), + spec: deployment + }]) + } + } + + const response = await fetch(`${apiUrl}/k8s/${resourceType}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }) + + const data = await response.json() + + if (data.success) { + setErrorMessage(`✅ ${data.message}`) + setTimeout(() => setErrorMessage(null), 5000) + setActionModal(null) + // Reload data to reflect the deletion + await loadData() + } else { + setErrorMessage(`❌ ${data.error || data.message || 'Failed to delete resource'}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to delete resource: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + // Handle pod selection toggle + const handlePodSelect = (podKey: string) => { + setSelectedPods(prev => { + const newSet = new Set(prev) + if (newSet.has(podKey)) { + newSet.delete(podKey) + } else { + newSet.add(podKey) + } + return newSet + }) + } + + // Handle select all pods + const handleSelectAllPods = () => { + if (selectedPods.size === pods.length) { + setSelectedPods(new Set()) + } else { + setSelectedPods(new Set(pods.map(p => `${p.namespace}/${p.name}`))) + } + } + + // Handle bulk delete pods + const handleBulkDeletePods = async () => { + if (selectedPods.size === 0) return + + if (!confirm(`Are you sure you want to delete ${selectedPods.size} pod(s)? This action cannot be undone.`)) { + return + } + + setIsDeletingPods(true) + setErrorMessage(null) + + let successCount = 0 + let failCount = 0 + + for (const podKey of selectedPods) { + const [namespace, name] = podKey.split('/') + try { + const response = await fetch(`${apiUrl}/k8s/pods/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + }) + const data = await response.json() + if (data.success) { + successCount++ + } else { + failCount++ + } + } catch { + failCount++ + } + } + + setSelectedPods(new Set()) + setIsDeletingPods(false) + + if (failCount === 0) { + setErrorMessage(`✅ Successfully deleted ${successCount} pod(s)`) + } else { + setErrorMessage(`⚠️ Deleted ${successCount} pod(s), failed to delete ${failCount} pod(s)`) + } + setTimeout(() => setErrorMessage(null), 5000) + await loadData() + } + + // Handle restore deleted deployment + const handleRestoreDeployment = async (deletedDeployment: { name: string; namespace: string; deletedAt: string; spec: any }) => { + if (!confirm(`Are you sure you want to restore deployment "${deletedDeployment.name}" in namespace "${deletedDeployment.namespace}"?`)) { + return + } + + setIsActioning(true) + setErrorMessage(null) + + try { + // Create deployment from saved spec + const response = await fetch(`${apiUrl}/k8s/deployments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + namespace: deletedDeployment.namespace, + name: deletedDeployment.name, + spec: deletedDeployment.spec + }), + }) + + const data = await response.json() + + if (data.success) { + // Remove from deleted deployments list + setDeletedDeployments(prev => prev.filter(d => + !(d.name === deletedDeployment.name && d.namespace === deletedDeployment.namespace && d.deletedAt === deletedDeployment.deletedAt) + )) + setErrorMessage(`✅ Successfully restored deployment "${deletedDeployment.name}"`) + setTimeout(() => setErrorMessage(null), 5000) + await loadData() + } else { + setErrorMessage(`❌ Failed to restore deployment: ${data.error || data.message}`) + } + } catch (error: any) { + setErrorMessage(`❌ Failed to restore deployment: ${error.message}`) + } finally { + setIsActioning(false) + } + } + + // Handle clear deleted deployment from history + const handleClearDeletedDeployment = (deletedDeployment: { name: string; namespace: string; deletedAt: string; spec: any }) => { + setDeletedDeployments(prev => prev.filter(d => + !(d.name === deletedDeployment.name && d.namespace === deletedDeployment.namespace && d.deletedAt === deletedDeployment.deletedAt) + )) + } + + // Auto-connect on component mount (using ServiceAccount) + useEffect(() => { + if (apiUrl && !isConnected) { + loadNamespaces(true); + } + }, [apiUrl]); + + // Get docker image tag from portal deployment for build version + const portalDeployment = deployments.find(d => d.name.toLowerCase().includes('portal')) + const dockerImageTag = portalDeployment && portalDeployment.containers?.length > 0 + ? portalDeployment.containers[0].image.split(':')[1] || 'latest' + : (deployments.length > 0 && deployments[0].containers?.length > 0 + ? deployments[0].containers[0].image.split(':')[1] || 'latest' + : 'latest') + + // ─── Helper: time ago ─── + const timeAgo = (dateStr: string) => { + if (!dateStr) return '-' + const now = new Date() + const date = new Date(dateStr) + const seconds = Math.floor((now.getTime() - date.getTime()) / 1000) + if (seconds < 0) return 'just now' + if (seconds < 60) return `${seconds}s ago` + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + return `${days}d ago` + } + + // ─── Fetch audit logs for overview ─── + useEffect(() => { + if (activeResourceTab === 'overview' && isConnected && apiUrl) { + fetch(`${apiUrl}/audit/logs?limit=10`) + .then(res => res.json()) + .then(data => { + if (data.success) setAuditLogs(data.logs || []) + }) + .catch(err => console.warn('Failed to fetch audit logs:', err)) + } + }, [activeResourceTab, isConnected, apiUrl]) + + // ─── Fetch pod for Describe modal ─── + useEffect(() => { + if (!describePod || !apiUrl) { + setDescribePodContent(null) + setDescribePodError(null) + return + } + let cancelled = false + setDescribePodLoading(true) + setDescribePodError(null) + setDescribePodContent(null) + fetch(`${apiUrl}/k8s/pods/${encodeURIComponent(describePod.namespace)}/${encodeURIComponent(describePod.name)}`, { + headers: getAuthHeaders(), + }) + .then(res => res.json()) + .then(data => { + if (cancelled) return + if (data.success && data.pod) { + setDescribePodContent(JSON.stringify(data.pod, null, 2)) + } else { + setDescribePodError(data.message || 'Failed to load pod') + } + }) + .catch(err => { + if (!cancelled) setDescribePodError(err.message || 'Failed to fetch pod') + }) + .finally(() => { + if (!cancelled) setDescribePodLoading(false) + }) + return () => { cancelled = true } + }, [describePod, apiUrl]) + + // ─── Global search keyboard shortcut (Cmd+K / Ctrl+K) ─── + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault() + setShowSearchModal(true) + setSearchQuery('') + } + if (e.key === 'Escape' && showSearchModal) { + setShowSearchModal(false) + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [showSearchModal]) + + // ─── Search results computation ─── + const searchResults = useMemo(() => { + if (!searchQuery.trim()) return [] + const q = searchQuery.toLowerCase() + const results: Array<{ type: string; name: string; namespace: string; tabKey: string; resourceType: string }> = [] + deployments.filter(d => d.name.toLowerCase().includes(q)).forEach(d => + results.push({ type: 'Deployment', name: d.name, namespace: d.namespace, tabKey: 'deployments', resourceType: 'deployment' })) + pods.filter(p => p.name.toLowerCase().includes(q)).forEach(p => + results.push({ type: 'Pod', name: p.name, namespace: p.namespace, tabKey: 'pods', resourceType: 'pod' })) + services.filter(s => s.name.toLowerCase().includes(q)).forEach(s => + results.push({ type: 'Service', name: s.name, namespace: s.namespace, tabKey: 'services', resourceType: 'service' })) + configmaps.filter(c => c.name?.toLowerCase().includes(q)).forEach(c => + results.push({ type: 'ConfigMap', name: c.name, namespace: c.namespace, tabKey: 'configmaps', resourceType: 'configmap' })) + secrets.filter(s => s.name?.toLowerCase().includes(q)).forEach(s => + results.push({ type: 'Secret', name: s.name, namespace: s.namespace, tabKey: 'secrets', resourceType: 'secret' })) + cronjobs.filter(c => c.name.toLowerCase().includes(q)).forEach(c => + results.push({ type: 'CronJob', name: c.name, namespace: c.namespace, tabKey: 'cronjobs', resourceType: 'cronjob' })) + jobs.filter(j => j.name.toLowerCase().includes(q)).forEach(j => + results.push({ type: 'Job', name: j.name, namespace: j.namespace, tabKey: 'jobs', resourceType: 'job' })) + ingresses.filter(i => i.name.toLowerCase().includes(q)).forEach(i => + results.push({ type: 'Ingress', name: i.name, namespace: i.namespace, tabKey: 'ingresses', resourceType: 'ingress' })) + pvcs.filter(p => p.name.toLowerCase().includes(q)).forEach(p => + results.push({ type: 'PVC', name: p.name, namespace: p.namespace, tabKey: 'pvcs', resourceType: 'pvc' })) + // Nodes + nodes.filter((n: any) => (n.name || n.metadata?.name || '').toLowerCase().includes(q)).forEach((n: any) => + results.push({ type: 'Node', name: n.name || n.metadata?.name, namespace: '', tabKey: 'overview', resourceType: 'node' })) + // Events + events.filter((e: any) => (e.message || '').toLowerCase().includes(q) || (e.reason || '').toLowerCase().includes(q) || (e.involvedObject?.name || '').toLowerCase().includes(q)).forEach((e: any) => + results.push({ type: 'Event', name: `${e.involvedObject?.kind || ''}/${e.involvedObject?.name || ''} — ${e.reason || ''}`, namespace: e.namespace || '', tabKey: 'events', resourceType: 'event' })) + // Databases from localStorage (connected database services) + try { + const savedDbs = localStorage.getItem('database_connections') + if (savedDbs) { + const dbConns = JSON.parse(savedDbs) as Array<{ name: string; type: string; host?: string; namespace?: string }> + dbConns.filter(db => (db.name || '').toLowerCase().includes(q) || (db.type || '').toLowerCase().includes(q) || (db.host || '').toLowerCase().includes(q)).forEach(db => + results.push({ type: 'Database', name: `${db.name} (${db.type})`, namespace: db.namespace || '', tabKey: 'databases', resourceType: 'database' })) + } + } catch {} + return results.slice(0, 50) + }, [searchQuery, deployments, pods, services, configmaps, secrets, cronjobs, jobs, ingresses, pvcs, nodes, events]) + + // ─── Notifications computation ─── + const notifications = useMemo(() => { + const notifs: Array<{ type: 'error' | 'warning'; message: string; resource: string; namespace: string; timestamp: string }> = [] + // Pods with CrashLoopBackOff or OOMKilled + pods.forEach(pod => { + if (pod.issues && Array.isArray(pod.issues)) { + pod.issues.forEach((issue: any) => { + if (issue.reason === 'CrashLoopBackOff' || issue.reason === 'OOMKilled') { + notifs.push({ type: 'error', message: `${issue.reason}: ${issue.message || pod.name}`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) + } + }) + } + }) + // Pods with any issues (hasIssues flag from API) + pods.forEach(pod => { + if (pod.hasIssues && (!pod.issues || pod.issues.length === 0)) { + notifs.push({ type: 'warning', message: `Pod ${pod.name} has issues (status: ${pod.status})`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) + } + }) + // Pods not in Running/Succeeded state + pods.forEach(pod => { + if (pod.status && pod.status !== 'Running' && pod.status !== 'Succeeded') { + // Avoid duplicate if already caught by issues above + const alreadyNotified = notifs.some(n => n.resource === pod.name && n.namespace === pod.namespace) + if (!alreadyNotified) { + notifs.push({ type: 'warning', message: `Pod ${pod.name} is ${pod.status}`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || pod.creationTimestamp || '' }) + } + } + }) + // Deployments with Stalled / ReplicaFailure / Unavailable / Degraded + deployments.forEach(dep => { + if (dep.deploymentStatus === 'Stalled' || dep.deploymentStatus === 'ReplicaFailure' || dep.deploymentStatus === 'Unavailable' || dep.deploymentStatus === 'Degraded') { + notifs.push({ type: 'error', message: `Deployment ${dep.name} is ${dep.deploymentStatus}`, resource: dep.name, namespace: dep.namespace, timestamp: dep.creationTimestamp }) + } + }) + // Warning events — check both lastTimestamp and eventTime, use 2-hour window + const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000) + events.forEach((event: any) => { + if (event.type === 'Warning') { + const eventTs = event.lastTimestamp || event.eventTime || event.firstTimestamp + if (eventTs && new Date(eventTs) > twoHoursAgo) { + notifs.push({ type: 'warning', message: `${event.reason}: ${(event.message || '').slice(0, 80)}`, resource: `${event.involvedObject?.kind || ''}/${event.involvedObject?.name || ''}`, namespace: event.namespace || '', timestamp: eventTs }) + } + } + }) + // Pods with high restart count (API returns "restarts" field, not "restartCount") + pods.forEach(pod => { + const restarts = (pod as any).restarts ?? (pod as any).restartCount ?? 0 + if (restarts > 5) { + // Avoid duplicate + const alreadyNotified = notifs.some(n => n.resource === pod.name && n.namespace === pod.namespace && n.message.includes('restarts')) + if (!alreadyNotified) { + notifs.push({ type: 'warning', message: `Pod ${pod.name} has ${restarts} restarts`, resource: pod.name, namespace: pod.namespace, timestamp: pod.startTime || '' }) + } + } + }) + return notifs.slice(0, 50) + }, [pods, deployments, events]) + + return ( +
+ {/* Left Sidebar - Fixed Dark Theme */} +
+ {/* Logo/Brand Section */} +
+
+
+ +
+ Klarity +
+
+ + {/* Project/Namespace Selector */} +
+
NAMESPACE
+
+ {selectedNamespace || 'Connecting...'} +
+
+ + {/* Navigation Menu */} + + + {/* Bottom Section */} +
+
Build version: {dockerImageTag}
+
+
+ + {/* Main Content Area - Full width with left margin for fixed sidebar */} +
+ {/* Connection Banner - Only when not connected */} + {!isConnected && ( +
+
+
+ + Connecting to Kubernetes cluster via ServiceAccount... +
+
+
+ )} + + {/* Main Content */} + {isConnected && ( +
+ {/* Header Banner - no overflow-hidden so notification dropdown is not clipped */} +
+
+ + + + + + + + +
+
+
+

Welcome to Klarity

+

Experience seamless Kubernetes management with powerful observability and control.

+
+
+ {/* Search Button */} + + {/* Notification Bell */} +
+ + {showNotificationPanel && ( +
+
+

Notifications ({notifications.length})

+ +
+
+ {notifications.length === 0 ? ( +
+ + All clear! No issues detected. +
+ ) : notifications.map((notif, idx) => { + const parts = (notif.resource || '').split('/') + const kind = parts.length > 1 ? parts[0] : 'Pod' + const name = parts.length > 1 ? parts[1] : notif.resource + const tabMap: Record = { Pod: 'pods', Deployment: 'deployments', Service: 'services' } + const typeMap: Record = { Pod: 'pod', Deployment: 'deployment', Service: 'service' } + const tab = tabMap[kind] || 'pods' + const resType = typeMap[kind] || 'pod' + return ( +
{ + setShowNotificationPanel(false) + setActiveResourceTab(tab as any) + if (name) setSelectedResource({ type: resType, namespace: notif.namespace || selectedNamespace, name }) + }} + > +
+ +
+

{notif.message}

+

{notif.resource} · {timeAgo(notif.timestamp)}

+
+
+
+ ) + })} +
+
+ )} +
+
+
+
+ + + + + +
+
+ + {/* Tabs Navigation */} +
+
+ + + {/* Databases tab hidden */} + {enableMongoDBWizard && ( + + )} + {enableAdminerWizard && ( + + )} + {enableImageChangeReport && ( + + )} +
+
+ + {/* Content Area */} +
+ {/* Quick Stats Cards + Resource Summary - hidden on Flow Tracing tab and all DB tabs */} + {activeResourceTab !== 'flow-tracing' && + activeResourceTab !== 'databases' && + activeResourceTab !== 'mongodb-wizard' && + activeResourceTab !== 'adminer-wizard' && ( + <> +
+
+

Cluster Resources

+ + + Connected + +
+
+ + {autoRefresh && ( + + )} + +
+
+ + {/* Compact Resource Summary Cards */} +
+
setActiveResourceTab('deployments')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'deployments' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Deployments +
+
{deployments.length}
+
+
setActiveResourceTab('pods')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'pods' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Pods +
+
{pods.length}
+
+
setActiveResourceTab('services')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'services' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Services +
+
{services.length}
+
+
setActiveResourceTab('cronjobs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'cronjobs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + CronJobs +
+
{cronjobs.length}
+
+
setActiveResourceTab('jobs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'jobs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Jobs +
+
{jobs.length}
+
+
setActiveResourceTab('events')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'events' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Events +
+
{events.length}
+
+
setActiveResourceTab('ingresses')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'ingresses' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + Ingresses +
+
{ingresses.length}
+
+
setActiveResourceTab('pvcs')} className={`bg-white rounded-lg border p-3 cursor-pointer transition-all hover:shadow ${activeResourceTab === 'pvcs' ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}`}> +
+ + PVCs +
+
{pvcs.length}
+
+
+ + )} + + {/* ═══ OVERVIEW TAB ═══ */} + {activeResourceTab === 'overview' && ( +
+ {/* Health Summary Cards - clickable, numbers centered */} +
+
setActiveResourceTab('deployments')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Deployments +
+
{deployments.length}
+
+ {deployments.filter(d => d.deploymentStatus === 'Ready').length} healthy + {deployments.filter(d => d.hasIssues).length > 0 && {deployments.filter(d => d.hasIssues).length} issues} +
+
+
setActiveResourceTab('pods')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Pods +
+
+ {pods.filter((p: any) => p.status === 'Running' && p.ready !== false).length} + /{pods.length} +
+
Running
+
+
setActiveResourceTab('services')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Services +
+
{services.length}
+
Active in namespace
+
+
setActiveResourceTab('events')} className="bg-white rounded-xl border border-gray-200 p-5 cursor-pointer transition-all hover:shadow-md hover:border-gray-300"> +
+ + Warning Events +
+
{events.filter((e: any) => e.type === 'Warning').length}
+
From cluster events
+
+
+ + {/* Deployment Status Distribution Bar */} +
+

Deployment Health

+ {(() => { + const total = deployments.length || 1 + const ready = deployments.filter(d => d.deploymentStatus === 'Ready').length + const progressing = deployments.filter(d => d.deploymentStatus === 'Progressing' || d.deploymentStatus === 'Updating').length + const stalled = deployments.filter(d => d.deploymentStatus === 'Stalled' || d.deploymentStatus === 'Degraded').length + const failed = deployments.filter(d => d.deploymentStatus === 'Unavailable' || d.deploymentStatus === 'ReplicaFailure').length + const other = total - ready - progressing - stalled - failed + return ( +
+
+ {ready > 0 &&
} + {progressing > 0 &&
} + {stalled > 0 &&
} + {failed > 0 &&
} + {other > 0 &&
} +
+
+ Ready: {ready} + {progressing > 0 && Progressing: {progressing}} + {stalled > 0 && Stalled: {stalled}} + {failed > 0 && Failed: {failed}} + {other > 0 && Other: {other}} +
+
+ ) + })()} +
+ + {/* Unhealthy Resources */} + {(() => { + const unhealthyPods = pods.filter((p: any) => p.hasIssues || p.status !== 'Running' || p.ready === false) + if (unhealthyPods.length === 0) return ( +
+ +

All pods are healthy and running.

+
+ ) + return ( +
+
+

⚠ Unhealthy Pods ({unhealthyPods.length})

+
+
+ + + + + + + + + + + + {unhealthyPods.map((pod: any) => ( + { setActiveResourceTab('pods'); setSelectedResource({ type: 'pod', namespace: pod.namespace, name: pod.name }) }}> + + + + + + + ))} + +
PodStatusReasonRestartsAge
{pod.name}{pod.status}{pod.issues?.[0]?.reason || (pod.ready === false ? 'Not Ready' : '-')}{pod.restartCount ?? 0}{timeAgo(pod.creationTimestamp || pod.startTime || '')}
+
+
+ ) + })()} + + {/* Recent Warning Events */} + {(() => { + const warningEvents = events.filter((e: any) => e.type === 'Warning') + if (warningEvents.length === 0) return null + const kindToTab: Record = { Pod: 'pods', Deployment: 'deployments', Service: 'services', ReplicaSet: 'deployments', Node: 'overview' } + const kindToType: Record = { Pod: 'pod', Deployment: 'deployment', Service: 'service', ReplicaSet: 'deployment', Node: null } + return ( +
+
+

Recent Warning Events

+
+
+ + + + + + + + + + + {warningEvents.map((event: any, idx: number) => { + const kind = event.involvedObject?.kind || '' + const name = event.involvedObject?.name || '' + const tab = kindToTab[kind] + const resType = kindToType[kind] + const ns = event.involvedObject?.namespace || event.metadata?.namespace || selectedNamespace || '' + return ( + { + if (tab) setActiveResourceTab(tab as any) + if (resType && name) setSelectedResource({ type: resType, namespace: ns || selectedNamespace, name }) + if (tab === 'overview' && kind === 'Node') setSelectedResource(null) + }} + > + + + + + + ) + })} + +
WhenResourceReasonMessage
{timeAgo(event.lastTimestamp || event.eventTime || '')}{event.involvedObject?.kind}/{event.involvedObject?.name}{event.reason}{event.message}
+
+
+ ) + })()} + + {/* Recent Activity from Audit Logs */} +
+
+

Recent Activity

+
+ {auditLogs.length === 0 ? ( +
No recent activity recorded.
+ ) : ( +
+ {auditLogs.map((log: any, idx: number) => { + const resourceType = (log.resourceType || '').toLowerCase() + const tabMap: Record = { deployments: 'deployments', pods: 'pods', services: 'services', configmaps: 'configmaps', secrets: 'secrets', cronjobs: 'cronjobs', jobs: 'jobs', ingresses: 'ingresses', pvcs: 'pvcs', namespaces: 'overview' } + const typeMap: Record = { deployments: 'deployment', pods: 'pod', services: 'service', configmaps: 'configmap', secrets: 'secret', cronjobs: 'cronjob', jobs: 'job', ingresses: 'ingress', pvcs: 'pvc', namespaces: null } + const tab = tabMap[resourceType] + const resType = typeMap[resourceType] + const ns = log.namespace || selectedNamespace || '' + return ( +
{ + if (tab) setActiveResourceTab(tab as any) + if (resType && log.resourceName) setSelectedResource({ type: resType, namespace: ns, name: log.resourceName }) + }} + > +
+ + {log.user || 'system'} + {log.action} + {log.resourceType}/{log.resourceName} +
+ {timeAgo(log.timestamp)} +
+ ) + })} +
+ )} +
+
+ )} + + {/* Bulk Update Controls - Show for deployments */} + {activeResourceTab === 'deployments' && deployments.length > 0 && ( +
+
+
+ Update Mode: + + +
+ {updateMode === 'bulk' && ( + + )} +
+ {imageUpdates.size > 0 && ( +
+ {imageUpdates.size} image(s) pending update +
+ + +
+
+ )} +
+ )} + + {/* Resource Details Section */} + {/* Detailed Resource View */} +
+ {/* Deployments Tab Content */} + {activeResourceTab === 'deployments' && ( +
+ {/* Deleted Deployments Restore Section - only when deployment delete enabled */} + {enableDeploymentDelete && deletedDeployments.length > 0 && ( +
+ + {showDeletedDeployments && ( +
+
+ Recently Deleted Deployments + (Session only - will be lost on page refresh) +
+
+ {deletedDeployments.map((deleted, idx) => ( +
+
+
{deleted.name}
+
+ Namespace: {deleted.namespace} | Deleted: {new Date(deleted.deletedAt).toLocaleString()} +
+
+
+ + +
+
+ ))} +
+
+ +
+
+ )} +
+ )} + {isLoading ? ( +
+
+ +
+

Loading deployments...

+
+ ) : deployments.length === 0 ? ( +
+
+ +
+

No deployments found

+

{selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}

+
+ ) : ( +
+ + + + {updateMode === 'selective' && ( + + )} + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + {deployments.map((deployment) => + (deployment.containers || []).map((container, idx) => { + const key = `${deployment.namespace}/${deployment.name}/${container.name}` + const update = imageUpdates.get(key) + const isSelected = selectedDeployments.has( + `${deployment.namespace}/${deployment.name}` + ) + + return ( + + {updateMode === 'selective' && idx === 0 && ( + + )} + {selectedNamespace === 'all' && idx === 0 && ( + + )} + {idx === 0 && ( + + )} + + + + {idx === 0 && ( + + )} + + ) + }) + )} + +
+ 0 && + deployments.every((d) => + selectedDeployments.has(`${d.namespace}/${d.name}`) + ) + } + onChange={(e) => { + if (e.target.checked) { + setSelectedDeployments( + new Set( + deployments.map((d) => `${d.namespace}/${d.name}`) + ) + ) + } else { + setSelectedDeployments(new Set()) + } + }} + className="w-4 h-4 rounded border-gray-300 bg-white text-blue-600 focus:ring-blue-500" + /> + + Namespace + + Deployment + + Container + + Current Image + + New Image + + Actions +
+ + toggleDeploymentSelection(deployment.namespace, deployment.name) + } + className="w-4 h-4 rounded border-gray-300 bg-white text-blue-600" + /> + + {deployment.namespace} + +
+ {deployment.name} +
+ {/* Status indicator based on deploymentStatus */} + {deployment.deploymentStatus === 'Ready' ? ( + + + Ready + + ) : deployment.deploymentStatus === 'Progressing' || deployment.deploymentStatus === 'Updating' ? ( + + + {deployment.deploymentStatus} + + ) : deployment.deploymentStatus === 'Stalled' ? ( + + + Stalled + + ) : deployment.deploymentStatus === 'ReplicaFailure' || deployment.deploymentStatus === 'Unavailable' ? ( + + + {deployment.deploymentStatus === 'ReplicaFailure' ? 'Failed' : 'Unavailable'} + + ) : deployment.deploymentStatus === 'Degraded' ? ( + + + Degraded + + ) : deployment.deploymentStatus === 'ScaledToZero' ? ( + + + Scaled to 0 + + ) : deployment.readyReplicas === deployment.replicas && deployment.replicas > 0 ? ( + + + Ready + + ) : deployment.readyReplicas > 0 ? ( + + + Partial + + ) : ( + + + Not Ready + + )} + {deployment.readyReplicas}/{deployment.replicas} +
+
+ {/* Warning triangle for deployments with issues */} + {deployment.hasIssues && ( +
+ + {expandedDeploymentIssues === `${deployment.namespace}-${deployment.name}` && ( +
+
+ + + Deployment Issues ({deployment.issues?.length || 0}) + + +
+
+ {deployment.statusReason && ( +
+
{deployment.statusReason}
+ {deployment.statusMessage && ( +
+ {deployment.statusMessage.length > 200 + ? `${deployment.statusMessage.substring(0, 200)}...` + : deployment.statusMessage} +
+ )} +
+ )} + {deployment.issues && deployment.issues.length > 0 && deployment.issues.map((issue, issueIdx) => ( +
+
+ + {issue.conditionType} + +
+
{issue.reason}
+ {issue.message && ( +
+ {issue.message.length > 150 + ? `${issue.message.substring(0, 150)}...` + : issue.message} +
+ )} +
+
+
+ ))} +
+ {/* Show deployment conditions */} + {deployment.conditions && deployment.conditions.some(c => c.status === 'False') && ( +
+
Conditions:
+
+ {deployment.conditions.filter(c => c.status === 'False').map((cond, condIdx) => ( +
+ + {cond.type}: + {cond.reason || 'Unknown'} +
+ ))} +
+
+ )} +
+ )} +
+ )} +
+ {container.name} + + {container.image} + +
+ + handleImageChange( + deployment.namespace, + deployment.name, + container.name, + e.target.value, + container.image + ) + } + placeholder={container.image} + title={update ? update.image : container.image} + className="flex-1 min-w-0 px-2 py-1.5 bg-white border border-gray-300 rounded text-[11px] font-mono text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + /> + {update && update.image !== container.image && ( + + )} +
+
+
+ + + + {enableDeploymentDelete && ( + + )} + +
+
+
+ )} + + + {imageUpdates.size > 0 && ( +
+
+ + {imageUpdates.size} container image(s) pending update + +
+ + +
+
+
+ )} +
+ )} + + {/* Pods Tab Content */} + {activeResourceTab === 'pods' && ( +
+ {/* Bulk Actions Bar */} + {selectedPods.size > 0 && ( +
+ + {selectedPods.size} pod(s) selected + +
+ + +
+
+ )} + {isLoading ? ( +
+
+ +
+

Loading pods...

+
+ ) : pods.length === 0 ? ( +
+
+ +
+

No pods found

+

{selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`}

+
+ ) : ( +
+ + + + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + + + {pods.map((pod) => { + const podKey = `${pod.namespace}/${pod.name}` + return ( + setSelectedResource({ type: 'pod', namespace: pod.namespace, name: pod.name })} + > + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + ) + })} + +
+ 0} + onChange={handleSelectAllPods} + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" + title="Select all pods" + /> + + Namespace + + Pod Name + + Status + + Node + + Pod IP + + Containers + + Restarts + + Actions +
e.stopPropagation()}> + handlePodSelect(podKey)} + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" + /> + + {pod.namespace} + +
+ {pod.name} + {pod.hasIssues && ( +
+ + {expandedPodIssues === `${pod.namespace}-${pod.name}` && ( +
+
+ + + Pod Issues ({pod.issues?.length || 0}) + + +
+
+ {pod.issues && pod.issues.length > 0 ? ( + pod.issues.map((issue, idx) => ( +
+
+ + {issue.type === 'initContainer' ? 'init' : issue.type} + +
+
+ {issue.reason} + {issue.exitCode !== undefined && ( + (exit: {issue.exitCode}) + )} +
+ {issue.containerName && ( +
+ Container: {issue.containerName} +
+ )} + {issue.conditionType && ( +
+ Condition: {issue.conditionType} +
+ )} + {issue.message && ( +
+ {issue.message.length > 150 + ? `${issue.message.substring(0, 150)}...` + : issue.message} +
+ )} +
+
+
+ )) + ) : ( +
+ Pod has issues but no specific details available. + Check container status and conditions. +
+ )} +
+ {/* Show pod conditions if available */} + {pod.conditions && pod.conditions.some(c => c.status === 'False') && ( +
+
Conditions:
+
+ {pod.conditions.filter(c => c.status === 'False').map((cond, idx) => ( +
+ + {cond.type}: + {cond.reason || 'Unknown'} +
+ ))} +
+
+ )} +
+ )} +
+ )} +
+
+ + + {pod.status} + + + + {pod.nodeName || '-'} + + + + {pod.podIP || '-'} + + +
+ {(pod.containers || []).map((container, idx) => ( +
+ + {container.name} + + {container.image.split('/').pop()} + +
+ ))} +
+
+ + {pod.restartCount || 0} + + +
e.stopPropagation()}> + + + + +
+
+
+ )} +
+ )} + + {/* Services Tab Content */} + {activeResourceTab === 'services' && ( +
+ {isLoading ? ( +
+ +

Loading services...

+
+ ) : services.length === 0 ? ( +
+ No services found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + {services.map((service) => ( + + + + + + + + ))} + +
Service NameTypeCluster IPPortsActions
setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {service.name} + setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {service.type} + setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {service.clusterIP || '-'} + setSelectedResource({ type: 'service', namespace: service.namespace, name: service.name })} + > + {(service.ports || []).map((port, idx) => ( +
+ {port.port}/{port.protocol} +
+ ))} +
e.stopPropagation()}> +
+ + {(service.ports && service.ports.length > 0) ? ( + + ) : ( + null + )} +
+
+
+ )} +
+ )} + + {/* ConfigMaps Tab Content */} + {activeResourceTab === 'configmaps' && ( +
+ {isLoading ? ( +
+ +

Loading configmaps...

+
+ ) : configmaps.length === 0 ? ( +
+ No configmaps found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + {configmaps.map((cm) => ( + setSelectedResource({ type: 'configmap', namespace: cm.namespace, name: cm.name })} + > + + + + + ))} + +
ConfigMap NameData KeysCreated
{cm.name} +
+ {Object.keys(cm.data || {}).map((key) => ( + + {key} + + ))} +
+
+ {cm.creationTimestamp ? new Date(cm.creationTimestamp).toLocaleDateString() : '-'} +
+
+ )} +
+ )} + + {/* Secrets Tab Content */} + {activeResourceTab === 'secrets' && ( +
+
+ +
+ {isLoading ? ( +
+ +

Loading secrets...

+
+ ) : secrets.length === 0 ? ( +
+ No secrets found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + {secrets.map((s) => ( + setSelectedResource({ type: 'secret', namespace: s.namespace, name: s.name })} + > + + + + + + ))} + +
Secret NameTypeKeysCreated
{s.name}{s.type || 'Opaque'} +
+ {(s.keys || []).map((key: string) => ( + + {key} + + ))} +
+
+ {s.age ? new Date(s.age).toLocaleDateString() : '-'} +
+
+ )} +
+ )} + + {/* Events Tab Content */} + {activeResourceTab === 'events' && ( +
+ {isLoading ? ( +
+ +

Loading events...

+
+ ) : events.length === 0 ? ( +
+ No events found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + {events.map((event) => ( + setSelectedEvent(event)} + > + + + + + + + + ))} + +
ObjectTypeReasonMessageCountLast Seen
+ {event.involvedObject?.kind}/{event.involvedObject?.name} + + + {event.type} + + {event.reason}{event.message}{event.count || 0} + {event.lastTimestamp ? new Date(event.lastTimestamp).toLocaleString() : '-'} +
+
+ )} +
+ )} + + {/* CronJobs Tab Content */} + {activeResourceTab === 'cronjobs' && ( +
+ {isLoading ? ( +
+ +

Loading cronjobs...

+
+ ) : cronjobs.length === 0 ? ( +
+ No cronjobs found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + + {cronjobs.map((cronjob) => ( + setSelectedResource({ type: 'cronjob', namespace: cronjob.namespace, name: cronjob.name })} + > + + + + + + + + + + ))} + +
NameScheduleSuspendActiveLast ScheduleContainersCreatedActions
{cronjob.name}{cronjob.schedule || '-'} + + {cronjob.suspend ? 'Yes' : 'No'} + + {cronjob.active} + {cronjob.lastScheduleTime ? new Date(cronjob.lastScheduleTime).toLocaleString() : '-'} + +
+ {(cronjob.containers || []).map((container) => ( + + {container.name}: {container.image.split(':')[0]} + + ))} +
+
+ {cronjob.creationTimestamp ? new Date(cronjob.creationTimestamp).toLocaleDateString() : '-'} + + +
+
+ )} +
+ )} + + {/* Jobs Tab Content */} + {activeResourceTab === 'jobs' && ( +
+ {isLoading ? ( +
+ +

Loading jobs...

+
+ ) : jobs.length === 0 ? ( +
+ No jobs found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + + {jobs.map((job) => { + const jobAge = job.startTime ? new Date(job.startTime) : null + const ageString = jobAge ? `${Math.floor((Date.now() - jobAge.getTime()) / (1000 * 60))}m` : '-' + return ( + setSelectedResource({ type: 'job', namespace: job.namespace, name: job.name })} + > + + + + + + + + + + ) + })} + +
NameCompletionsSucceededFailedActiveAgeContainersActions
{job.name}{job.succeeded}/{job.completions} + + {job.succeeded} + + + {job.failed > 0 ? ( + + {job.failed} + + ) : ( + 0 + )} + + {job.active > 0 ? ( + + {job.active} + + ) : ( + 0 + )} + {ageString} +
+ {(job.containers || []).map((container) => ( + + {container.name}: {container.image.split(':')[0]} + + ))} +
+
+ +
+
+ )} +
+ )} +
+ + {/* Flow Tracing Tab Content */} + {activeResourceTab === 'flow-tracing' && ( +
+ +
+ )} + + {/* Databases Tab Content - only when feature enabled */} + {enableDatabase && activeResourceTab === 'databases' && ( +
+ +
+ )} + + {/* MongoDB Compass Tab Content */} + {enableMongoDBWizard && activeResourceTab === 'mongodb-wizard' && ( +
+ +
+ )} + + {/* Adminer Tab Content */} + {enableAdminerWizard && activeResourceTab === 'adminer-wizard' && ( +
+ +
+ )} + + {/* ═══ INGRESSES TAB ═══ */} + {activeResourceTab === 'ingresses' && ( +
+ {isLoading ? ( +
+ +

Loading ingresses...

+
+ ) : ingresses.length === 0 ? ( +
+ No ingresses found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + {ingresses.map((ing) => ( + + + + + + + + + + ))} + +
NameClassHostsPathsBackendTLSAge
{ing.name}{ing.ingressClassName || —} + {ing.rules.map((r, i) =>
{r.host}
)} +
+
+ {ing.rules.flatMap((r, ri) => r.paths.map((p, pi) => ( + {p.path} + )))} +
+
+
+ {ing.rules.flatMap((r, ri) => r.paths.map((p, pi) => ( + p.backend?.serviceName ?
{p.backend.serviceName}:{p.backend.servicePort}
: null + )))} +
+
+ {ing.tls && ing.tls.length > 0 ? ( + 🔒 TLS + ) : ( + — + )} + {ing.creationTimestamp ? timeAgo(ing.creationTimestamp) : '-'}
+
+ )} +
+ )} + + {/* ═══ PVCs TAB ═══ */} + {activeResourceTab === 'pvcs' && ( +
+ {isLoading ? ( +
+ +

Loading persistent volume claims...

+
+ ) : pvcs.length === 0 ? ( +
+ No PVCs found in namespace "{selectedNamespace}" +
+ ) : ( +
+ + + + + + + + + + + + + + {pvcs.map((pvc) => ( + + + + + + + + + + ))} + +
NameStatusStorage ClassCapacityAccess ModesVolumeAge
{pvc.name} + {pvc.status} + {pvc.storageClassName || —}{pvc.capacity || '—'} +
+ {pvc.accessModes.map((mode) => ( + {mode} + ))} +
+
{pvc.volumeName || '—'}{pvc.creationTimestamp ? timeAgo(pvc.creationTimestamp) : '-'}
+
+ )} +
+ )} + + {/* Image change report: per-deployment image change count + which images (from cluster) - only when feature enabled */} + {enableImageChangeReport && activeResourceTab === 'image-change-report' && ( +
+
+

Image change report

+

Per deployment: how many times the image was changed (count) and which images (flow from start to current). Changes made via Selective or Bulk Update in Deployments are reflected here. Download CSV available.

+ {(deployedNamespace || selectedNamespace) && (deployedNamespace || selectedNamespace) !== 'all' ? ( + <> +
+
+ Namespace: + {deployedNamespace || selectedNamespace} +
+ +
+ {imageChangeReportData && ( + <> + {/* Current deployment images from cluster - always shown when we have data */} + {imageChangeReportData.currentDeployments && imageChangeReportData.currentDeployments.length > 0 && ( +
+
+ + Current deployment images in namespace {deployedNamespace || selectedNamespace} (from cluster — change count & flow from ReplicaSet history) + + +
+ + + + + + + + + + + {imageChangeReportData.currentDeployments.map((row, i) => ( + + + + + + + ))} + +
Service (Deployment)Image changesCurrent image listFlow (start → current)
{row.deployment}{row.changeCount ?? 0} +
    + {row.images?.map((img, j) => ( +
  • {img.container ? `${img.container}: ${img.image}` : img.image}
  • + ))} +
+
+ {row.imageFlow && row.imageFlow.length > 0 ? ( +
+ {row.imageFlow.map((rev, idx) => ( + + {idx > 0 && →} + im.image).join(', ')}> + Rev{rev.revision ?? idx + 1} ({rev.createdAt ? new Date(rev.createdAt).toISOString().slice(0, 10) : '-'}): {rev.images?.map((im) => im.image?.split(':').pop() || im.image).join(', ')} + + + ))} +
+ ) : ( + — + )} +
+
+ )} + + {/* Recent Image Changes - Detailed History by Deployment (collapsible) */} + {imageChangeReportData.recentInNamespace && imageChangeReportData.recentInNamespace.length > 0 && (() => { + const recent = imageChangeReportData.recentInNamespace + const byDeployment = recent.reduce((acc: Record, entry) => { + const key = entry.deployment || 'unknown' + if (!acc[key]) acc[key] = [] + acc[key].push(entry) + return acc + }, {}) + const deploymentNames = Object.keys(byDeployment) + const toggleExpanded = (key: string) => { + setExpandedImageHistoryDeployments(prev => { + const next = new Set(prev) + if (next.has(key)) next.delete(key) + else next.add(key) + return next + }) + } + return ( +
+
+ + Recent image changes — by deployment ({recent.length} entries) + + +
+ {deploymentNames.map((depName) => { + const entries = byDeployment[depName] + const count = entries.length + const isExpanded = expandedImageHistoryDeployments.has(depName) + return ( +
+ + {isExpanded && ( +
+ + + + + + + + + + + + + {entries.map((entry, i) => { + const sourceLabel = entry.source === 'ui-single' ? 'UI - Single Edit' : entry.source === 'ui-bulk' ? 'UI - Bulk Update' : entry.source === 'ui-selective' ? 'UI - Selective' : entry.source === 'api' ? 'API / Terminal' : entry.source || 'Unknown' + const sourceBg = entry.source === 'ui-single' ? 'bg-green-100 text-green-800' : entry.source === 'ui-bulk' ? 'bg-purple-100 text-purple-800' : entry.source === 'ui-selective' ? 'bg-blue-100 text-blue-800' : entry.source === 'api' ? 'bg-orange-100 text-orange-800' : 'bg-gray-100 text-gray-700' + return ( + + + + + + + + + ) + })} + +
WhenDeploymentContainerOld Image → New ImageSourceChanged By
+
{entry.at ? new Date(entry.at).toLocaleDateString() : entry.date}
+
{entry.at ? new Date(entry.at).toLocaleTimeString() : ''}
+
{entry.deployment}{entry.container || '—'} +
+ {entry.oldImage ? ( + <> + {entry.oldImage.length > 40 ? '...' + entry.oldImage.slice(-37) : entry.oldImage} + → + {(entry.image || '').length > 40 ? '...' + (entry.image || '').slice(-37) : entry.image} + + ) : ( + {(entry.image || '').length > 50 ? '...' + (entry.image || '').slice(-47) : entry.image} + )} +
+
+ {sourceLabel} + {entry.user || 'anonymous'}
+
+ )} +
+ ) + })} +
+ ) + })()} + + {/* No audit data message */} + {imageChangeReportData.recentInNamespace && imageChangeReportData.recentInNamespace.length === 0 && ( +
+ No image change history found in audit logs. Changes made via the UI will appear here after you update images. +
+ )} + + )} + + ) : ( +
+ Connect to the cluster to see the image change report for your deployed namespace. +
+ )} +
+
+ )} + + {/* Download Images - Auto Excel Download */} + {activeResourceTab === 'download-images' && (() => { + // Auto-trigger Excel download when this tab is selected + const downloadImagesExcel = () => { + // Find max containers in any deployment + const maxContainers = Math.max(...deployments.map(d => d.containers.length), 1) + + // Build header with Container 1, Container 2, Container 3 columns + const header = ['S.No', 'Deployment', 'Namespace'] + for (let i = 1; i <= maxContainers; i++) { + header.push(`Container ${i} Name`, `Container ${i} Image`, `Container ${i} Tag`) + } + + const rows: string[][] = [header] + + let sno = 1 + deployments.forEach(d => { + const row: string[] = [ + String(sno++), + d.name, + d.namespace + ] + + // Add each container in separate columns + for (let i = 0; i < maxContainers; i++) { + if (d.containers[i]) { + const imageParts = d.containers[i].image.split(':') + const imageName = imageParts[0] + const imageTag = imageParts[1] || 'latest' + row.push(d.containers[i].name, imageName, imageTag) + } else { + row.push('', '', '') + } + } + + rows.push(row) + }) + + // Convert to CSV format (Excel compatible) + const csvContent = rows.map(row => + row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',') + ).join('\n') + + // Create and download file + const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }) + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.setAttribute('href', url) + link.setAttribute('download', `${selectedNamespace}-images-${new Date().toISOString().split('T')[0]}.csv`) + link.style.visibility = 'hidden' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // Switch back to deployments tab after download + setTimeout(() => setActiveResourceTab('deployments'), 100) + } + + // Trigger download immediately + setTimeout(downloadImagesExcel, 100) + + return ( +
+
+
+ + + +
+

Downloading Images List...

+

+ Exporting {deployments.reduce((acc, d) => acc + d.containers.length, 0)} container images from {selectedNamespace} namespace +

+
+
+ ) + })()} + +
+
+ )} +
+ + {/* Old Pods Table - Removed (now in tabbed pane) */} + {false && isConnected && selectedNamespace && showPods && ( +
+
+

+ Pods ({pods.length}) {selectedNamespace === 'all' ? '- All Namespaces' : `- Namespace: ${selectedNamespace}`} +

+ +
+ + {isLoading ? ( +
+ +

Loading pods...

+
+ ) : pods.length === 0 ? ( +
+ No pods found {selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`} +
+ ) : ( +
+ + + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + + + {pods.map((pod) => ( + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + ))} + +
+ Namespace + + Pod Name + + Status + + Node + + Pod IP + + Containers + + Restarts + + Actions +
+ {pod.namespace} + +
+ {pod.name} + {pod.hasIssues && ( +
+ + {expandedPodIssues === `${pod.namespace}-${pod.name}` && ( +
+
+ + + Pod Issues ({pod.issues?.length || 0}) + + +
+
+ {pod.issues && pod.issues.length > 0 ? ( + pod.issues.map((issue, idx) => ( +
+
+ + {issue.type === 'initContainer' ? 'init' : issue.type} + +
+
+ {issue.reason} + {issue.exitCode !== undefined && ( + (exit: {issue.exitCode}) + )} +
+ {issue.containerName && ( +
+ Container: {issue.containerName} +
+ )} + {issue.conditionType && ( +
+ Condition: {issue.conditionType} +
+ )} + {issue.message && ( +
+ {issue.message.length > 150 + ? `${issue.message.substring(0, 150)}...` + : issue.message} +
+ )} +
+
+
+ )) + ) : ( +
+ Pod has issues but no specific details available. + Check container status and conditions. +
+ )} +
+ {/* Show pod conditions if available */} + {pod.conditions && pod.conditions.some(c => c.status === 'False') && ( +
+
Conditions:
+
+ {pod.conditions.filter(c => c.status === 'False').map((cond, idx) => ( +
+ + {cond.type}: + {cond.reason || 'Unknown'} +
+ ))} +
+
+ )} +
+ )} +
+ )} +
+
+ + {pod.status} + + + {pod.nodeName || '-'} + + {pod.podIP || '-'} + +
+ {(pod.containers || []).map((container, idx) => ( +
+ {container.name}:{' '} + {container.image} + {container.ready !== undefined && ( + + {container.ready ? '✓' : '✗'} + + )} +
+ ))} +
+
+ {pod.restartCount || 0} + +
+ +
+
+
+ )} +
+ )} + + + {/* Services Table with Edit */} + {/* Old Services Table - Removed (now in tabbed pane) */} + {false && isConnected && selectedNamespace && showServices && ( +
+
+

+ Services ({services.length}) {selectedNamespace === 'all' ? '- All Namespaces' : `- Namespace: ${selectedNamespace}`} +

+ +
+ + {isLoading ? ( +
+ +

Loading services...

+
+ ) : services.length === 0 ? ( +
+ No services found {selectedNamespace === 'all' ? 'in any namespace' : `in namespace "${selectedNamespace}"`} +
+ ) : ( +
+ + + + {selectedNamespace === 'all' && ( + + )} + + + + + + + + + {services.map((service) => ( + + {selectedNamespace === 'all' && ( + + )} + + + + + + + ))} + +
+ Namespace + + Service + + Type + + Cluster IP + + Ports + + Selector +
+ {service.namespace} + + {service.name} + + + {service.type} + + + {service.clusterIP || '-'} + + {(service.ports || []).map((port, idx) => ( +
+ {port.port}:{port.targetPort}/{port.protocol} + {port.name && ` (${port.name})`} +
+ ))} +
+
+ {Object.entries(service.selector).map(([key, value]) => ( + + {key}={value} + + ))} +
+
+
+ )} +
+ )} + + {/* Port Forward Modal (OpenLens-style) */} + {portForwardModal && ( +
+
+
setPortForwardModal(null)} /> +
+
+
+ +

Port forward

+
+ +
+

+ {portForwardModal.service.name} + · + {portForwardModal.service.namespace} +

+ +
+
+ + +
+
+ + setPortForwardModal({ ...portForwardModal, localPort: Number(e.target.value) || portForwardModal.targetPort })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm" + /> +
+
+ + + + Open in browser (no kubectl) + + +
+

Or run in terminal

+
+ + kubectl port-forward -n {portForwardModal.service.namespace} svc/{portForwardModal.service.name} {portForwardModal.localPort}:{portForwardModal.targetPort} + + +
+
+
+
+
+ )} + + {/* Resource Detail Modal */} + setSelectedResource(null)} + resourceType={selectedResource?.type || null} + namespace={selectedResource?.namespace || ''} + name={selectedResource?.name || ''} + apiUrl={apiUrl} + onResourceDeleted={loadData} + /> + + {/* Describe Pod Modal */} + {describePod && ( +
+
+
setDescribePod(null)} aria-hidden /> +
+
+

+ Describe Pod: {describePod.name} + ({describePod.namespace}) +

+ +
+
+ {describePodLoading && ( +
+ +
+ )} + {describePodError && ( +

{describePodError}

+ )} + {!describePodLoading && !describePodError && describePodContent && ( +
+                    {describePodContent}
+                  
+ )} +
+
+
+
+ )} + + {/* Create Secret Modal */} + {showCreateSecretModal && ( +
+
+
setShowCreateSecretModal(false)} /> +
+

Create Secret

+

+ Namespace: {selectedNamespace} (key-value pairs are stored as base64) +

+
+ + setCreateSecretName(e.target.value)} + placeholder="my-secret" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-amber-500 focus:border-amber-500" + /> +
+
+ + {createSecretData.map((row, idx) => ( +
+ { + const next = [...createSecretData] + next[idx] = { ...next[idx], key: e.target.value } + setCreateSecretData(next) + }} + placeholder="key" + className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono" + /> + { + const next = [...createSecretData] + next[idx] = { ...next[idx], value: e.target.value } + setCreateSecretData(next) + }} + placeholder="value" + className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono" + /> + +
+ ))} + +
+ {createSecretError && ( +
+ {createSecretError} +
+ )} +
+ + +
+
+
+
+ )} + + {selectedLogPod && ( + setSelectedLogPod(null)} + podName={selectedLogPod.name} + namespace={selectedLogPod.namespace} + container={selectedLogPod.container} + apiUrl={apiUrl} + /> + )} + + {/* Event Detail Modal */} + {selectedEvent && ( +
+
+
+
+

Event Details

+

+ {selectedEvent.involvedObject?.kind}/{selectedEvent.involvedObject?.name} +

+
+ +
+
+
+
+ +
+ + {selectedEvent.type} + +
+
+
+ +

{selectedEvent.reason}

+
+
+ +

{selectedEvent.count || 1}

+
+
+ +

+ {selectedEvent.source?.component || '-'} / {selectedEvent.source?.host || '-'} +

+
+
+ +
+ +

{selectedEvent.message}

+
+ +
+
+ +

+ {selectedEvent.firstTimestamp ? new Date(selectedEvent.firstTimestamp).toLocaleString() : '-'} +

+
+
+ +

+ {selectedEvent.lastTimestamp ? new Date(selectedEvent.lastTimestamp).toLocaleString() : '-'} +

+
+
+ +
+ +
+

Kind: {selectedEvent.involvedObject?.kind}

+

Name: {selectedEvent.involvedObject?.name}

+

Namespace: {selectedEvent.involvedObject?.namespace}

+ {selectedEvent.involvedObject?.uid && ( +

UID: {selectedEvent.involvedObject.uid}

+ )} +
+
+ + {selectedEvent.name && ( +
+ +

{selectedEvent.name}

+
+ )} +
+
+
+ )} + + {/* Bulk Update Image Wizard Modal */} + {showBulkUpdateWizard && ( +
+
+
+
+

Bulk Image Update Wizard

+

+ {bulkUpdateStep === 'select' + ? 'Step 1: Select deployments to update' + : 'Step 2: Review and apply updates'} +

+
+ +
+ +
+ {bulkUpdateStep === 'select' ? ( +
+
+ +
+ {bulkUpdateSelections.size} deployment(s) selected +
+
+ +
+ + + + + + + + + + + {deployments.map((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + const isSelected = bulkUpdateSelections.has(key) + return ( + + + + + + + ) + })} + +
DeploymentContainersCurrent Images
+ toggleBulkUpdateSelection(deployment.namespace, deployment.name)} + className="w-4 h-4" + /> + +
{deployment.name}
+
{deployment.namespace}
+
+
+ {(deployment.containers || []).map((container, idx) => ( +
+ {container.name} +
+ ))} +
+
+
+ {(deployment.containers || []).map((container, idx) => ( +
+ {container.image} +
+ ))} +
+
+
+ + {/* Container Tabs Section - Only show if deployments are selected */} + {bulkUpdateSelections.size > 0 && (() => { + // Group all containers by type (sidecar vs main) + const mainContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + const sidecarContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + const isSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecar) { + sidecarContainers.push({ deployment, container }) + } else { + mainContainers.push({ deployment, container }) + } + }) + } + }) + + // Set initial active tab if not set + if (!bulkUpdateActiveTab) { + if (mainContainers.length > 0) { + setBulkUpdateActiveTab('main') + } else if (sidecarContainers.length > 0) { + setBulkUpdateActiveTab('sidecar') + } + } + + const isActiveSidecar = bulkUpdateActiveTab === 'sidecar' + const activeContainers = isActiveSidecar ? sidecarContainers : mainContainers + const activeImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' + const activeTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false + + return ( +
+ {/* Container Type Tabs */} +
+
+ {mainContainers.length > 0 && ( + + )} + {sidecarContainers.length > 0 && ( + + )} +
+
+ + {/* Selected Container Type's Deployments Table */} + {activeContainers.length > 0 && ( +
+
+ + {isActiveSidecar ? 'Sidecar' : 'Main'} Containers ({activeContainers.length} total) + +
+ + + + + + + + + + + {activeContainers.map((item, idx) => ( + + + + + + + ))} + +
DeploymentNamespaceContainer NameCurrent Image
+
{item.deployment.name}
+
+
{item.deployment.namespace}
+
+
{item.container.name}
+
+
{item.container.image}
+
+
+ )} + + {/* Image Input Section - Different for sidecar vs main containers */} +
+
+ { + const newMap = new Map(bulkUpdateContainerTagOnly) + newMap.set(bulkUpdateActiveTab, e.target.checked) + setBulkUpdateContainerTagOnly(newMap) + }} + className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + +
+
+ + { + const newMap = new Map(bulkUpdateContainerImages) + newMap.set(bulkUpdateActiveTab, e.target.value) + setBulkUpdateContainerImages(newMap) + }} + placeholder={activeTagOnly ? "e.g., 1.12.0-rc5 or latest" : "e.g., nginx:1.25 or myregistry.io/app:v2.0"} + className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm" + /> + {activeTagOnly && ( +

+ Only the tag portion will be updated. Base image will remain unchanged. +

+ )} +
+
+
+ ) + })()} +
+ ) : ( +
+ {(() => { + // Group all containers by type (sidecar vs main) + const mainContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + const sidecarContainers: Array<{ + deployment: Deployment + container: { name: string; image: string } + }> = [] + + deployments.forEach((deployment) => { + const key = `${deployment.namespace}/${deployment.name}` + if (bulkUpdateSelections.has(key)) { + deployment.containers.forEach((container) => { + const isSidecar = container.name.toLowerCase().includes('sidecar') + if (isSidecar) { + sidecarContainers.push({ deployment, container }) + } else { + mainContainers.push({ deployment, container }) + } + }) + } + }) + + const isActiveSidecar = bulkUpdateActiveTab === 'sidecar' + const activeContainers = isActiveSidecar ? sidecarContainers : mainContainers + const activeImage = bulkUpdateContainerImages.get(bulkUpdateActiveTab) || '' + const activeTagOnly = bulkUpdateContainerTagOnly.get(bulkUpdateActiveTab) || false + + return ( + <> + {/* Container Type Tabs */} +
+
+ {mainContainers.length > 0 && ( + + )} + {sidecarContainers.length > 0 && ( + + )} +
+
+ +
+

Update Summary

+
+

• {bulkUpdateSelections.size} deployment(s) selected

+

• Container Type: {isActiveSidecar ? 'Sidecar' : 'Main'}

+

• Containers to update: {activeContainers.length}

+

• New {activeTagOnly ? 'tag' : 'image'}: {activeImage}

+
+
+ +
+
+ + + + + + + + + + + + {activeContainers.map((item, idx) => { + const newImage = activeTagOnly + ? applyTagOnlyUpdate(item.container.image, activeImage.trim()) + : activeImage.trim() + return ( + + + + + + + + ) + })} + +
DeploymentNamespaceContainer NameCurrent ImageNew Image
{item.deployment.name}{item.deployment.namespace}{item.container.name}{item.container.image} + {newImage} +
+
+
+ + ) + })()} +
+ )} +
+ +
+ +
+ {bulkUpdateStep === 'select' ? ( + + ) : ( + + )} +
+
+
+
+ )} + + {/* Image comparison modal */} + {showImageComparison && ( +
+
setShowImageComparison(false)} + /> +
+
+
+

Image update review

+

+ Compare current vs new images before applying updates +

+
+ +
+
+ {imageUpdates.size === 0 ? ( +

No pending image updates.

+ ) : ( + + + + + + + + + + + + {Array.from(imageUpdates.values()).map((update, idx) => ( + + + + + + + + ))} + +
NamespaceDeploymentContainerCurrent ImageNew Image
{update.namespace}{update.deployment}{update.container}{update.originalImage}{update.image}
+ )} +
+
+ + {imageUpdates.size} change(s) ready to apply + +
+ + +
+
+
+
+ )} + + {/* Action Confirmation Modals */} + {actionModal && ( +
+
+
setActionModal(null)}>
+ +
+
+
+

+ {actionModal.type === 'scale' && 'Scale Deployment'} + {actionModal.type === 'restart' && 'Restart Deployment'} + {actionModal.type === 'delete' && 'Delete Resource'} +

+ +
+ + {actionModal.type === 'scale' && ( +
+

+ Scale deployment {actionModal.name} in namespace {actionModal.namespace} +

+
+ + setScaleReplicas(Math.max(0, parseInt(e.target.value) || 0))} + className="w-20 px-3 py-2 border border-gray-300 rounded-md text-center focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + replicas +
+ {actionModal.currentReplicas !== undefined && ( +

+ Current: {actionModal.currentReplicas} replicas +

+ )} +
+ )} + + {actionModal.type === 'restart' && ( +
+

+ Are you sure you want to restart deployment {actionModal.name} in namespace {actionModal.namespace}? +

+

+ This will trigger a rolling restart of all pods in the deployment. +

+
+ )} + + {actionModal.type === 'delete' && ( +
+
+ +

+ Are you sure you want to delete {actionModal.resourceType} {actionModal.name} in namespace {actionModal.namespace}? +

+
+

+ This action cannot be undone! +

+
+ )} + +
+ + +
+
+
+
+
+ )} + {/* ═══ GLOBAL SEARCH MODAL (Cmd+K) ═══ */} + {showSearchModal && ( +
+
setShowSearchModal(false)} /> +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search deployments, pods, services, databases, events..." + className="flex-1 text-sm text-gray-900 placeholder-gray-400 bg-transparent outline-none" + /> + ESC +
+
+ {searchQuery.trim() === '' ? ( +
+ + Type to search across all resource types... +
+ ) : searchResults.length === 0 ? ( +
No results for "{searchQuery}"
+ ) : ( +
+ {searchResults.map((result, idx) => ( + + ))} +
+ )} +
+
+
+ )} +
+ ) +} diff --git a/benchmarks/pairs/agent-vfc/market-cap-route-4f8ae9/fixed.ts b/benchmarks/pairs/agent-vfc/market-cap-route-4f8ae9/fixed.ts new file mode 100644 index 0000000..a50e17a --- /dev/null +++ b/benchmarks/pairs/agent-vfc/market-cap-route-4f8ae9/fixed.ts @@ -0,0 +1,31 @@ +// Provenance: navaneethbv/market-cap (fixed). +// repo: navaneethbv/market-cap +// commit: 4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6 +// parent: fec6f024ce72c9c47b61cbac6ce7a35cb2280602 +// commit_url: https://github.com/navaneethbv/market-cap/commit/4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6 +// cve: +// license: MIT +// function: GET +// relpath: app/auth/confirm/route.ts +// provenance: agent +// mechanism: source_reaches_sink + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const token_hash = searchParams.get("token_hash"); + const type = searchParams.get("type") as EmailOtpType | null; + const nextParam = searchParams.get("next") ?? "/"; + // Only allow same-origin relative paths to prevent open redirects + const next = + nextParam.startsWith("/") && !nextParam.startsWith("//") ? nextParam : "/"; + + if (token_hash && type) { + const supabase = await createClient(); + const { error } = await supabase.auth.verifyOtp({ type, token_hash }); + if (!error) { + return NextResponse.redirect(new URL(next, request.url)); + } + } + + return NextResponse.redirect(new URL("/login?error=confirm", request.url)); +} diff --git a/benchmarks/pairs/agent-vfc/market-cap-route-4f8ae9/vuln.ts b/benchmarks/pairs/agent-vfc/market-cap-route-4f8ae9/vuln.ts new file mode 100644 index 0000000..8ba90dc --- /dev/null +++ b/benchmarks/pairs/agent-vfc/market-cap-route-4f8ae9/vuln.ts @@ -0,0 +1,28 @@ +// Provenance: navaneethbv/market-cap (vuln). +// repo: navaneethbv/market-cap +// commit: fec6f024ce72c9c47b61cbac6ce7a35cb2280602 +// parent: fec6f024ce72c9c47b61cbac6ce7a35cb2280602 +// commit_url: https://github.com/navaneethbv/market-cap/commit/4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6 +// cve: +// license: MIT +// function: GET +// relpath: app/auth/confirm/route.ts +// provenance: agent +// mechanism: source_reaches_sink + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const token_hash = searchParams.get("token_hash"); + const type = searchParams.get("type") as EmailOtpType | null; + const next = searchParams.get("next") ?? "/"; + + if (token_hash && type) { + const supabase = await createClient(); + const { error } = await supabase.auth.verifyOtp({ type, token_hash }); + if (!error) { + return NextResponse.redirect(new URL(next, request.url)); + } + } + + return NextResponse.redirect(new URL("/login?error=confirm", request.url)); +} diff --git a/benchmarks/pairs/agent-vfc/melodix-playerprovider-089931/fixed.tsx b/benchmarks/pairs/agent-vfc/melodix-playerprovider-089931/fixed.tsx new file mode 100644 index 0000000..c5909f9 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/melodix-playerprovider-089931/fixed.tsx @@ -0,0 +1,126 @@ +// Provenance: alexshao3/melodix (fixed). +// repo: alexshao3/melodix +// commit: 089931ef71ea1e5b7586520baf1a5498e2f05b13 +// parent: c074fc7413338aefcbea3170ef4d3ba486a8a4b7 +// commit_url: https://github.com/alexshao3/melodix/commit/089931ef71ea1e5b7586520baf1a5498e2f05b13 +// cve: +// license: MIT +// function: PlayerProvider +// relpath: apps/miniapp/src/components/PlayerProvider.tsx +// provenance: agent +// mechanism: permissive_default + +export function PlayerProvider({ children }: { children: React.ReactNode }) { + const audioRef = useRef(null); + const [currentTrack, setCurrentTrack] = useState(null); + const [queue, setQueue] = useState([]); + const [history, setHistory] = useState([]); + const [isPlaying, setIsPlaying] = useState(false); + const [position, setPosition] = useState(0); + const [duration, setDuration] = useState(0); + const handleEndedRef = useRef<() => void>(() => {}); + + useEffect(() => { + const a = new Audio(); + a.preload = 'metadata'; + audioRef.current = a; + const t = () => setPosition(a.currentTime); + const l = () => setDuration(a.duration || 0); + const p = () => setIsPlaying(true); + const ps = () => setIsPlaying(false); + const e = () => handleEndedRef.current?.(); + a.addEventListener('timeupdate', t); + a.addEventListener('loadedmetadata', l); + a.addEventListener('play', p); + a.addEventListener('pause', ps); + a.addEventListener('ended', e); + return () => { + a.pause(); + a.removeEventListener('timeupdate', t); + a.removeEventListener('loadedmetadata', l); + a.removeEventListener('play', p); + a.removeEventListener('pause', ps); + a.removeEventListener('ended', e); + }; + }, []); + + const playTrack = useCallback((t: Track) => { + const a = audioRef.current; + if (!a) return; + a.src = t.streamUrl ?? t.audioUrl; + setCurrentTrack(t); + setPosition(0); + pushRecentlyPlayed(t); + // Best-effort server-side history mirror; `recordPlay` no-ops for guests. + void api.recordPlay(t.id); + a.play().catch(() => undefined); + }, []); + + const play = useCallback( + (track: Track, newQueue?: Track[]) => { + tgHaptic('light'); + if (newQueue) { + const idx = newQueue.findIndex((t) => t.id === track.id); + const upcoming = + idx >= 0 ? newQueue.slice(idx + 1) : newQueue.filter((t) => t.id !== track.id); + setQueue(upcoming); + } + if (currentTrack && currentTrack.id !== track.id) { + setHistory((h) => [...h.slice(-19), currentTrack]); + } + playTrack(track); + }, + [currentTrack, playTrack], + ); + + const toggle = useCallback(() => { + const a = audioRef.current; + if (!a || !currentTrack) return; + tgHaptic('light'); + if (a.paused) a.play(); + else a.pause(); + }, [currentTrack]); + + const next = useCallback(() => { + if (queue.length === 0) return; + const [head, ...rest] = queue; + if (!head) return; + if (currentTrack) setHistory((h) => [...h.slice(-19), currentTrack]); + setQueue(rest); + playTrack(head); + }, [queue, currentTrack, playTrack]); + + const prev = useCallback(() => { + const a = audioRef.current; + if (a && a.currentTime > 3) { + a.currentTime = 0; + return; + } + const last = history[history.length - 1]; + if (!last) { + if (a) a.currentTime = 0; + return; + } + setHistory((h) => h.slice(0, -1)); + if (currentTrack) setQueue((q) => [currentTrack, ...q]); + playTrack(last); + }, [history, currentTrack, playTrack]); + + useEffect(() => { + handleEndedRef.current = () => next(); + }, [next]); + + const seek = useCallback((s: number) => { + const a = audioRef.current; + if (!a) return; + a.currentTime = s; + setPosition(s); + }, []); + + const value = useMemo( + () => ({ currentTrack, queue, isPlaying, position, duration, play, toggle, next, prev, seek }), + [currentTrack, queue, isPlaying, position, duration, play, toggle, next, prev, seek], + ); + + return {children}; +} diff --git a/benchmarks/pairs/agent-vfc/melodix-playerprovider-089931/vuln.tsx b/benchmarks/pairs/agent-vfc/melodix-playerprovider-089931/vuln.tsx new file mode 100644 index 0000000..c326560 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/melodix-playerprovider-089931/vuln.tsx @@ -0,0 +1,127 @@ +// Provenance: alexshao3/melodix (vuln). +// repo: alexshao3/melodix +// commit: c074fc7413338aefcbea3170ef4d3ba486a8a4b7 +// parent: c074fc7413338aefcbea3170ef4d3ba486a8a4b7 +// commit_url: https://github.com/alexshao3/melodix/commit/089931ef71ea1e5b7586520baf1a5498e2f05b13 +// cve: +// license: MIT +// function: PlayerProvider +// relpath: apps/miniapp/src/components/PlayerProvider.tsx +// provenance: agent +// mechanism: permissive_default + +export function PlayerProvider({ children }: { children: React.ReactNode }) { + const audioRef = useRef(null); + const [currentTrack, setCurrentTrack] = useState(null); + const [queue, setQueue] = useState([]); + const [history, setHistory] = useState([]); + const [isPlaying, setIsPlaying] = useState(false); + const [position, setPosition] = useState(0); + const [duration, setDuration] = useState(0); + const handleEndedRef = useRef<() => void>(() => {}); + + useEffect(() => { + const a = new Audio(); + a.crossOrigin = 'anonymous'; + a.preload = 'metadata'; + audioRef.current = a; + const t = () => setPosition(a.currentTime); + const l = () => setDuration(a.duration || 0); + const p = () => setIsPlaying(true); + const ps = () => setIsPlaying(false); + const e = () => handleEndedRef.current?.(); + a.addEventListener('timeupdate', t); + a.addEventListener('loadedmetadata', l); + a.addEventListener('play', p); + a.addEventListener('pause', ps); + a.addEventListener('ended', e); + return () => { + a.pause(); + a.removeEventListener('timeupdate', t); + a.removeEventListener('loadedmetadata', l); + a.removeEventListener('play', p); + a.removeEventListener('pause', ps); + a.removeEventListener('ended', e); + }; + }, []); + + const playTrack = useCallback((t: Track) => { + const a = audioRef.current; + if (!a) return; + a.src = t.streamUrl ?? t.audioUrl; + setCurrentTrack(t); + setPosition(0); + pushRecentlyPlayed(t); + // Best-effort server-side history mirror; `recordPlay` no-ops for guests. + void api.recordPlay(t.id); + a.play().catch(() => undefined); + }, []); + + const play = useCallback( + (track: Track, newQueue?: Track[]) => { + tgHaptic('light'); + if (newQueue) { + const idx = newQueue.findIndex((t) => t.id === track.id); + const upcoming = + idx >= 0 ? newQueue.slice(idx + 1) : newQueue.filter((t) => t.id !== track.id); + setQueue(upcoming); + } + if (currentTrack && currentTrack.id !== track.id) { + setHistory((h) => [...h.slice(-19), currentTrack]); + } + playTrack(track); + }, + [currentTrack, playTrack], + ); + + const toggle = useCallback(() => { + const a = audioRef.current; + if (!a || !currentTrack) return; + tgHaptic('light'); + if (a.paused) a.play(); + else a.pause(); + }, [currentTrack]); + + const next = useCallback(() => { + if (queue.length === 0) return; + const [head, ...rest] = queue; + if (!head) return; + if (currentTrack) setHistory((h) => [...h.slice(-19), currentTrack]); + setQueue(rest); + playTrack(head); + }, [queue, currentTrack, playTrack]); + + const prev = useCallback(() => { + const a = audioRef.current; + if (a && a.currentTime > 3) { + a.currentTime = 0; + return; + } + const last = history[history.length - 1]; + if (!last) { + if (a) a.currentTime = 0; + return; + } + setHistory((h) => h.slice(0, -1)); + if (currentTrack) setQueue((q) => [currentTrack, ...q]); + playTrack(last); + }, [history, currentTrack, playTrack]); + + useEffect(() => { + handleEndedRef.current = () => next(); + }, [next]); + + const seek = useCallback((s: number) => { + const a = audioRef.current; + if (!a) return; + a.currentTime = s; + setPosition(s); + }, []); + + const value = useMemo( + () => ({ currentTrack, queue, isPlaying, position, duration, play, toggle, next, prev, seek }), + [currentTrack, queue, isPlaying, position, duration, play, toggle, next, prev, seek], + ); + + return {children}; +} diff --git a/benchmarks/pairs/agent-vfc/mercado-do-vale-news-catalogmetadataservice-9b5fcf/fixed.ts b/benchmarks/pairs/agent-vfc/mercado-do-vale-news-catalogmetadataservice-9b5fcf/fixed.ts new file mode 100644 index 0000000..1958cac --- /dev/null +++ b/benchmarks/pairs/agent-vfc/mercado-do-vale-news-catalogmetadataservice-9b5fcf/fixed.ts @@ -0,0 +1,46 @@ +// Provenance: handielson/Mercado-do-vale-news (fixed). +// repo: handielson/Mercado-do-vale-news +// commit: 9b5fcf2dae41e56f5f97952266f2d6ba59598598 +// parent: 70ca282b819ac1d60f4b5e665d224b1043ff1ccc +// commit_url: https://github.com/handielson/Mercado-do-vale-news/commit/9b5fcf2dae41e56f5f97952266f2d6ba59598598 +// cve: +// license: MIT +// function: fetchMetadata +// relpath: services/catalogMetadataService.ts +// provenance: agent +// mechanism: permissive_default + +async function fetchMetadata(): Promise { + // Deduplicação de requisições concorrentes + if (pendingFetch) return pendingFetch; + + pendingFetch = (async () => { + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 10000); + + // Bypass completo de cache de CDN e Browser + const timestamp = Date.now(); + const res = await fetch(buildVpsUrl(`/catalog/metadata?_t=${timestamp}`), { + signal: controller.signal, + headers: { Accept: 'application/json' }, + cache: 'no-store', + }); + clearTimeout(timer); + if (!res.ok) return null; + const data = await res.json(); + return { + categories: data.categories || [], + brands: data.brands || [], + priceRange: data.priceRange || null, + timestamp: Date.now(), + }; + } catch { + return null; + } finally { + pendingFetch = null; + } + })(); + + return pendingFetch; +} diff --git a/benchmarks/pairs/agent-vfc/mercado-do-vale-news-catalogmetadataservice-9b5fcf/vuln.ts b/benchmarks/pairs/agent-vfc/mercado-do-vale-news-catalogmetadataservice-9b5fcf/vuln.ts new file mode 100644 index 0000000..4b1cbd8 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/mercado-do-vale-news-catalogmetadataservice-9b5fcf/vuln.ts @@ -0,0 +1,50 @@ +// Provenance: handielson/Mercado-do-vale-news (vuln). +// repo: handielson/Mercado-do-vale-news +// commit: 70ca282b819ac1d60f4b5e665d224b1043ff1ccc +// parent: 70ca282b819ac1d60f4b5e665d224b1043ff1ccc +// commit_url: https://github.com/handielson/Mercado-do-vale-news/commit/9b5fcf2dae41e56f5f97952266f2d6ba59598598 +// cve: +// license: MIT +// function: fetchMetadata +// relpath: services/catalogMetadataService.ts +// provenance: agent +// mechanism: permissive_default + +async function fetchMetadata(): Promise { + // Deduplicação de requisições concorrentes + if (pendingFetch) return pendingFetch; + + pendingFetch = (async () => { + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 10000); + + // Bypass completo de cache de CDN e Browser + const timestamp = Date.now(); + const res = await fetch(buildVpsUrl(`/catalog/metadata?_t=${timestamp}`), { + signal: controller.signal, + headers: { + Accept: 'application/json', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache' + }, + cache: 'no-store', + }); + clearTimeout(timer); + if (!res.ok) return null; + const data = await res.json(); + return { + categories: data.categories || [], + brands: data.brands || [], + priceRange: data.priceRange || null, + timestamp: Date.now(), + }; + } catch { + return null; + } finally { + pendingFetch = null; + } + })(); + + return pendingFetch; +} diff --git a/benchmarks/pairs/agent-vfc/money-track-category-icons-4d4a5b/fixed.ts b/benchmarks/pairs/agent-vfc/money-track-category-icons-4d4a5b/fixed.ts new file mode 100644 index 0000000..54a2515 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/money-track-category-icons-4d4a5b/fixed.ts @@ -0,0 +1,15 @@ +// Provenance: andrebubniak/money-track (fixed). +// repo: andrebubniak/money-track +// commit: 4d4a5b063278c8b6697c3faae133d7c39fcd01c0 +// parent: add96df6d6a6b61506635c23dbacf6c6cfe24dc2 +// commit_url: https://github.com/andrebubniak/money-track/commit/4d4a5b063278c8b6697c3faae133d7c39fcd01c0 +// cve: +// license: MIT +// function: isCategoryIcon +// relpath: src/lib/category-icons.ts +// provenance: agent +// mechanism: prototype_pollution + +export function isCategoryIcon(value: string): value is keyof typeof CATEGORY_ICONS { + return Object.hasOwn(CATEGORY_ICONS, value); +} diff --git a/benchmarks/pairs/agent-vfc/money-track-category-icons-4d4a5b/vuln.ts b/benchmarks/pairs/agent-vfc/money-track-category-icons-4d4a5b/vuln.ts new file mode 100644 index 0000000..93a3a62 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/money-track-category-icons-4d4a5b/vuln.ts @@ -0,0 +1,15 @@ +// Provenance: andrebubniak/money-track (vuln). +// repo: andrebubniak/money-track +// commit: add96df6d6a6b61506635c23dbacf6c6cfe24dc2 +// parent: add96df6d6a6b61506635c23dbacf6c6cfe24dc2 +// commit_url: https://github.com/andrebubniak/money-track/commit/4d4a5b063278c8b6697c3faae133d7c39fcd01c0 +// cve: +// license: MIT +// function: isCategoryIcon +// relpath: src/lib/category-icons.ts +// provenance: agent +// mechanism: prototype_pollution + +export function isCategoryIcon(value: string): value is keyof typeof CATEGORY_ICONS { + return value in CATEGORY_ICONS; +} diff --git a/benchmarks/pairs/agent-vfc/nestjs-filter-normalizer-aa0e60/fixed.ts b/benchmarks/pairs/agent-vfc/nestjs-filter-normalizer-aa0e60/fixed.ts new file mode 100644 index 0000000..b3f6b31 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/nestjs-filter-normalizer-aa0e60/fixed.ts @@ -0,0 +1,27 @@ +// Provenance: DavideCarvalho/nestjs-filter (fixed). +// repo: DavideCarvalho/nestjs-filter +// commit: aa0e600d05997ea3d77538ea1c2db427c439f2e8 +// parent: d899b40d3466f2ffbe9f01c4d49267412401f078 +// commit_url: https://github.com/DavideCarvalho/nestjs-filter/commit/aa0e600d05997ea3d77538ea1c2db427c439f2e8 +// cve: +// license: MIT +// function: normalizeInput +// relpath: packages/core/src/input/normalizer.ts +// provenance: agent +// mechanism: prototype_pollution + +export function normalizeInput(input: unknown, options: NormalizeOptions): Record { + if (input == null || typeof input !== 'object') return {}; + const norm = pickNormalizer(options.normalizer); + const drop = options.dropId === true; + const out: Record = {}; + for (const [rawKey, value] of Object.entries(input as Record)) { + if (BLOCKED_KEYS.has(rawKey)) continue; + let key = norm(rawKey); + if (BLOCKED_KEYS.has(key)) continue; + if (drop) key = stripId(key); + if (key.length === 0) continue; + out[key] = value; + } + return out; +} diff --git a/benchmarks/pairs/agent-vfc/nestjs-filter-normalizer-aa0e60/vuln.ts b/benchmarks/pairs/agent-vfc/nestjs-filter-normalizer-aa0e60/vuln.ts new file mode 100644 index 0000000..6b38e1b --- /dev/null +++ b/benchmarks/pairs/agent-vfc/nestjs-filter-normalizer-aa0e60/vuln.ts @@ -0,0 +1,25 @@ +// Provenance: DavideCarvalho/nestjs-filter (vuln). +// repo: DavideCarvalho/nestjs-filter +// commit: d899b40d3466f2ffbe9f01c4d49267412401f078 +// parent: d899b40d3466f2ffbe9f01c4d49267412401f078 +// commit_url: https://github.com/DavideCarvalho/nestjs-filter/commit/aa0e600d05997ea3d77538ea1c2db427c439f2e8 +// cve: +// license: MIT +// function: normalizeInput +// relpath: packages/core/src/input/normalizer.ts +// provenance: agent +// mechanism: prototype_pollution + +export function normalizeInput(input: unknown, options: NormalizeOptions): Record { + if (input == null || typeof input !== 'object') return {}; + const norm = pickNormalizer(options.normalizer); + const drop = options.dropId === true; + const out: Record = {}; + for (const [rawKey, value] of Object.entries(input as Record)) { + let key = norm(rawKey); + if (drop) key = stripId(key); + if (key.length === 0) continue; + out[key] = value; + } + return out; +} diff --git a/benchmarks/pairs/agent-vfc/new-erp-final-app-6e9644/fixed.ts b/benchmarks/pairs/agent-vfc/new-erp-final-app-6e9644/fixed.ts new file mode 100644 index 0000000..ee38a71 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/new-erp-final-app-6e9644/fixed.ts @@ -0,0 +1,72 @@ +// Provenance: Sachin-Salgar/NEW_ERP_FINAL (fixed). +// repo: Sachin-Salgar/NEW_ERP_FINAL +// commit: 6e9644c81ce3fb356669157182a5ef09a70c4b59 +// parent: 08976c5d7a07b0663669b875b4507446199f85d7 +// commit_url: https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59 +// cve: +// license: Apache-2.0 +// function: origin +// relpath: src/presentation/http/app.ts +// provenance: agent +// mechanism: permissive_default + +export async function createApplication(config: AppConfig, providedPool?: Pool): Promise { + const app = Fastify({ + logger: createLogger(config), + requestIdHeader: 'x-request-id', + requestIdLogLabel: 'requestId', + ignoreTrailingSlash: true, + ajv: { + customOptions: { + allErrors: true, + coerceTypes: true, + removeAdditional: false, + }, + }, + }); + + const pool = providedPool ?? createDatabasePool(config); + const repository = new IdentityAwarePostgresPlatformRepository(pool); + const passwordHasher = new BcryptPasswordHasher(); + const jwtTokenService = new JwtTokenService(config); + const authService = new AuthenticationService(repository, passwordHasher, jwtTokenService); + const authorizationService = new AuthorizationService(repository); + const branchService = new BranchService(repository); + const coreEnterpriseService = new CoreEnterpriseService(repository); + const locationService = new LocationService(repository); + const moduleAccessService = new ModuleAccessService(pool); + const registrationService = new UserRegistrationService(repository, passwordHasher); + const tenantMembershipService = new TenantMembershipService(repository); + + app.decorate('appConfig', config); + app.decorate('authService', authService); + app.decorate('authorizationService', authorizationService); + app.decorate('branchService', branchService); + app.decorate('coreEnterpriseService', coreEnterpriseService); + app.decorate('locationService', locationService); + app.decorate('moduleAccessService', moduleAccessService); + app.decorate('registrationService', registrationService); + app.decorate('jwtTokenService', jwtTokenService); + app.decorate('tenantMembershipService', tenantMembershipService); + + await app.register(cors, { + origin: (origin, callback) => { + callback(null, isCorsOriginAllowed(config, origin)); + }, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'x-tenant-id'], + credentials: true, + optionsSuccessStatus: 204, + }); + + app.setErrorHandler(buildErrorHandler(config)); + + await app.register(healthRoutes, { prefix: config.API_PREFIX }); + await app.register(authRoutes, { prefix: config.API_PREFIX }); + await app.register(rbacRoutes, { prefix: config.API_PREFIX }); + await app.register(branchRoutes, { prefix: config.API_PREFIX }); + await app.register(coreEnterpriseRoutes, { prefix: config.API_PREFIX }); + await app.register(locationRoutes, { prefix: config.API_PREFIX }); + + return app; +} diff --git a/benchmarks/pairs/agent-vfc/new-erp-final-app-6e9644/vuln.ts b/benchmarks/pairs/agent-vfc/new-erp-final-app-6e9644/vuln.ts new file mode 100644 index 0000000..d9c450f --- /dev/null +++ b/benchmarks/pairs/agent-vfc/new-erp-final-app-6e9644/vuln.ts @@ -0,0 +1,72 @@ +// Provenance: Sachin-Salgar/NEW_ERP_FINAL (vuln). +// repo: Sachin-Salgar/NEW_ERP_FINAL +// commit: 08976c5d7a07b0663669b875b4507446199f85d7 +// parent: 08976c5d7a07b0663669b875b4507446199f85d7 +// commit_url: https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59 +// cve: +// license: Apache-2.0 +// function: origin +// relpath: src/presentation/http/app.ts +// provenance: agent +// mechanism: permissive_default + +export async function createApplication(config: AppConfig, providedPool?: Pool): Promise { + const app = Fastify({ + logger: createLogger(config), + requestIdHeader: 'x-request-id', + requestIdLogLabel: 'requestId', + ignoreTrailingSlash: true, + ajv: { + customOptions: { + allErrors: true, + coerceTypes: true, + removeAdditional: false, + }, + }, + }); + + const pool = providedPool ?? createDatabasePool(config); + const repository = new IdentityAwarePostgresPlatformRepository(pool); + const passwordHasher = new BcryptPasswordHasher(); + const jwtTokenService = new JwtTokenService(config); + const authService = new AuthenticationService(repository, passwordHasher, jwtTokenService); + const authorizationService = new AuthorizationService(repository); + const branchService = new BranchService(repository); + const coreEnterpriseService = new CoreEnterpriseService(repository); + const locationService = new LocationService(repository); + const moduleAccessService = new ModuleAccessService(pool); + const registrationService = new UserRegistrationService(repository, passwordHasher); + const tenantMembershipService = new TenantMembershipService(repository); + + app.decorate('appConfig', config); + app.decorate('authService', authService); + app.decorate('authorizationService', authorizationService); + app.decorate('branchService', branchService); + app.decorate('coreEnterpriseService', coreEnterpriseService); + app.decorate('locationService', locationService); + app.decorate('moduleAccessService', moduleAccessService); + app.decorate('registrationService', registrationService); + app.decorate('jwtTokenService', jwtTokenService); + app.decorate('tenantMembershipService', tenantMembershipService); + + await app.register(cors, { + origin: (origin, callback) => { + callback(null, isCorsOriginAllowed(config, origin)); + }, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'x-tenant-id'], + credentials: true, + optionsSuccessStatus: 204, + }); + + app.setErrorHandler(buildErrorHandler(config)); + + await app.register(healthRoutes, { prefix: config.API_PREFIX }); + await app.register(authRoutes, { prefix: config.API_PREFIX }); + await app.register(rbacRoutes, { prefix: config.API_PREFIX }); + await app.register(branchRoutes, { prefix: config.API_PREFIX }); + await app.register(coreEnterpriseRoutes, { prefix: config.API_PREFIX }); + await app.register(locationRoutes, { prefix: config.API_PREFIX }); + + return app; +} diff --git a/benchmarks/pairs/agent-vfc/nimestream-route-d17f53/fixed.js b/benchmarks/pairs/agent-vfc/nimestream-route-d17f53/fixed.js new file mode 100644 index 0000000..fe173cc --- /dev/null +++ b/benchmarks/pairs/agent-vfc/nimestream-route-d17f53/fixed.js @@ -0,0 +1,100 @@ +// Provenance: Irnhakim/nimestream (fixed). +// repo: Irnhakim/nimestream +// commit: d17f535bf3012041b056ff3aa991ff6cfa198e43 +// parent: 0c37bfd9e5292b15b1d7af51f52793d9bdf1cef0 +// commit_url: https://github.com/Irnhakim/nimestream/commit/d17f535bf3012041b056ff3aa991ff6cfa198e43 +// cve: +// license: MIT +// function: GET +// relpath: app/api/img/route.js +// provenance: agent +// mechanism: source_reaches_sink + +export async function GET(request) { + const { searchParams } = new URL(request.url); + const imageUrl = searchParams.get('url'); + + if (!imageUrl) { + return new Response('Missing url parameter', { status: 400 }); + } + + // Only allow proxying images from allowed domains for security + const allowed = [ + 'otakudesu.blog', 'otakudesu.cloud', 'otakudesu.ltd', + 'i3.wp.com', 'i2.wp.com', 'i1.wp.com', 'i0.wp.com', + 'cdn.otakudesu', 'kusonime.com', 'wp-content', + 'oploverz.ac', 'oploverz.site', 'backapi.oploverz', 'alqanime.net', + 'anichin.cafe', 'samehadaku', 'animasu', 'zoronime', 'anoboy', 'nimegami', 'animeindo', 'animekuindo', 'winbu', 'kuramanime', 'animekompi', 'donghub', 'dramabox' + ]; + + let parsedUrl; + try { + parsedUrl = new URL(imageUrl); + } catch (e) { + return new Response('Invalid URL', { status: 400 }); + } + + const hostname = parsedUrl.hostname; + const isAllowed = allowed.some(domain => + hostname === domain || hostname.endsWith(`.${domain}`) + ); + if (!isAllowed) { + return new Response('Forbidden', { status: 403 }); + } + + // Determine proper referer + let referer = `${BASE_URL}/`; + if (imageUrl.includes('kusonime.com')) { + referer = 'https://kusonime.com/'; + } else if (imageUrl.includes('oploverz.ac') || imageUrl.includes('oploverz.site')) { + referer = 'https://oploverz.site/'; + } else if (imageUrl.includes('alqanime.net')) { + referer = 'https://alqanime.net/'; + } else if (imageUrl.includes('anichin.cafe')) { + referer = 'https://anichin.cafe/'; + } else if (imageUrl.includes('samehadaku')) { + referer = 'https://samehadaku.vip/'; + } else if (imageUrl.includes('animasu')) { + referer = 'https://animasu.cc/'; + } else if (imageUrl.includes('kuramanime')) { + referer = 'https://kuramanime.net/'; + } + + try { + // Add abort controller to prevent long timeout waits + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 4000); // 4 seconds timeout limit + + const response = await fetch(imageUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': referer, + 'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate, br', + }, + signal: controller.signal, + cache: 'no-store', + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + return new Response('Failed to fetch source image', { status: response.status }); + } + + const contentType = response.headers.get('content-type') || 'image/jpeg'; + const imageBuffer = await response.arrayBuffer(); + + return new Response(imageBuffer, { + status: 200, + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=43200', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (err) { + console.error('Image proxy error:', err.message); + return new Response('Error loading image', { status: 500 }); + } +} diff --git a/benchmarks/pairs/agent-vfc/nimestream-route-d17f53/vuln.js b/benchmarks/pairs/agent-vfc/nimestream-route-d17f53/vuln.js new file mode 100644 index 0000000..2a6c8a0 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/nimestream-route-d17f53/vuln.js @@ -0,0 +1,90 @@ +// Provenance: Irnhakim/nimestream (vuln). +// repo: Irnhakim/nimestream +// commit: 0c37bfd9e5292b15b1d7af51f52793d9bdf1cef0 +// parent: 0c37bfd9e5292b15b1d7af51f52793d9bdf1cef0 +// commit_url: https://github.com/Irnhakim/nimestream/commit/d17f535bf3012041b056ff3aa991ff6cfa198e43 +// cve: +// license: MIT +// function: GET +// relpath: app/api/img/route.js +// provenance: agent +// mechanism: source_reaches_sink + +export async function GET(request) { + const { searchParams } = new URL(request.url); + const imageUrl = searchParams.get('url'); + + if (!imageUrl) { + return new Response('Missing url parameter', { status: 400 }); + } + + // Only allow proxying images from allowed domains for security + const allowed = [ + 'otakudesu.blog', 'otakudesu.cloud', 'otakudesu.ltd', + 'i3.wp.com', 'i2.wp.com', 'i1.wp.com', 'i0.wp.com', + 'cdn.otakudesu', 'kusonime.com', 'wp-content', + 'oploverz.ac', 'oploverz.site', 'backapi.oploverz', 'alqanime.net', + 'anichin.cafe', 'samehadaku', 'animasu', 'zoronime', 'anoboy', 'nimegami', 'animeindo', 'animekuindo', 'winbu', 'kuramanime', 'animekompi', 'donghub', 'dramabox' + ]; + const isAllowed = allowed.some(domain => imageUrl.includes(domain)); + if (!isAllowed) { + return new Response('Forbidden', { status: 403 }); + } + + // Determine proper referer + let referer = `${BASE_URL}/`; + if (imageUrl.includes('kusonime.com')) { + referer = 'https://kusonime.com/'; + } else if (imageUrl.includes('oploverz.ac') || imageUrl.includes('oploverz.site')) { + referer = 'https://oploverz.site/'; + } else if (imageUrl.includes('alqanime.net')) { + referer = 'https://alqanime.net/'; + } else if (imageUrl.includes('anichin.cafe')) { + referer = 'https://anichin.cafe/'; + } else if (imageUrl.includes('samehadaku')) { + referer = 'https://samehadaku.vip/'; + } else if (imageUrl.includes('animasu')) { + referer = 'https://animasu.cc/'; + } else if (imageUrl.includes('kuramanime')) { + referer = 'https://kuramanime.net/'; + } + + try { + // Add abort controller to prevent long timeout waits + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 4000); // 4 seconds timeout limit + + const response = await fetch(imageUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': referer, + 'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate, br', + }, + signal: controller.signal, + cache: 'no-store', + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + // Fallback redirect to direct URL if fetch failed but URL is reachable in browser + return Response.redirect(imageUrl, 302); + } + + const contentType = response.headers.get('content-type') || 'image/jpeg'; + const imageBuffer = await response.arrayBuffer(); + + return new Response(imageBuffer, { + status: 200, + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=43200', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (err) { + console.error('Image proxy error, falling back to direct redirect:', err.message); + return Response.redirect(imageUrl, 302); + } +} diff --git a/benchmarks/pairs/agent-vfc/openniw-jobs-0fc947/fixed.py b/benchmarks/pairs/agent-vfc/openniw-jobs-0fc947/fixed.py new file mode 100644 index 0000000..4cf688b --- /dev/null +++ b/benchmarks/pairs/agent-vfc/openniw-jobs-0fc947/fixed.py @@ -0,0 +1,28 @@ +# Provenance: HHHHHejia/openniw (fixed). +# repo: HHHHHejia/openniw +# commit: 0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10 +# parent: 4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc +# commit_url: https://github.com/HHHHHejia/openniw/commit/0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10 +# cve: +# license: MIT +# function: get_job +# relpath: backend/app/routers/jobs.py +# provenance: agent +# mechanism: identity_from_request_body + +async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict: + job = await jobs_service.get(job_id) + if job is None: + raise HTTPException(404, "Job not found") + # Authorization: case-bound jobs are visible only to the case owner; + # caseless jobs (free evaluations) are served by the public route only. + if job.get("case_id") is None: + raise HTTPException(404, "Job not found") + await auth.case_owned_by(str(job["case_id"]), user) + return { + "id": str(job["id"]), + "kind": job["kind"], + "status": job["status"], + "result": job["result"], + "error": job["error"], + } diff --git a/benchmarks/pairs/agent-vfc/openniw-jobs-0fc947/vuln.py b/benchmarks/pairs/agent-vfc/openniw-jobs-0fc947/vuln.py new file mode 100644 index 0000000..d1a077e --- /dev/null +++ b/benchmarks/pairs/agent-vfc/openniw-jobs-0fc947/vuln.py @@ -0,0 +1,23 @@ +# Provenance: HHHHHejia/openniw (vuln). +# repo: HHHHHejia/openniw +# commit: 4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc +# parent: 4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc +# commit_url: https://github.com/HHHHHejia/openniw/commit/0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10 +# cve: +# license: MIT +# function: get_job +# relpath: backend/app/routers/jobs.py +# provenance: agent +# mechanism: identity_from_request_body + +async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict: + job = await jobs_service.get(job_id) + if job is None: + raise HTTPException(404, "Job not found") + return { + "id": str(job["id"]), + "kind": job["kind"], + "status": job["status"], + "result": job["result"], + "error": job["error"], + } diff --git a/benchmarks/pairs/agent-vfc/openniw-jobs-b74de3/fixed.py b/benchmarks/pairs/agent-vfc/openniw-jobs-b74de3/fixed.py new file mode 100644 index 0000000..9a17c89 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/openniw-jobs-b74de3/fixed.py @@ -0,0 +1,28 @@ +# Provenance: ziyuzuo/openniw (fixed). +# repo: ziyuzuo/openniw +# commit: b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68 +# parent: 98a0c392ba96cd2c4aa746dd6fc2b183f385225e +# commit_url: https://github.com/ziyuzuo/openniw/commit/b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68 +# cve: +# license: MIT +# function: get_job +# relpath: backend/app/routers/jobs.py +# provenance: agent +# mechanism: identity_from_request_body + +async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict: + job = await jobs_service.get(job_id) + if job is None: + raise HTTPException(404, "Job not found") + # Authorization: case-bound jobs are visible only to the case owner; + # caseless jobs (free evaluations) are served by the public route only. + if job.get("case_id") is None: + raise HTTPException(404, "Job not found") + await auth.case_owned_by(str(job["case_id"]), user) + return { + "id": str(job["id"]), + "kind": job["kind"], + "status": job["status"], + "result": job["result"], + "error": job["error"], + } diff --git a/benchmarks/pairs/agent-vfc/openniw-jobs-b74de3/vuln.py b/benchmarks/pairs/agent-vfc/openniw-jobs-b74de3/vuln.py new file mode 100644 index 0000000..357731e --- /dev/null +++ b/benchmarks/pairs/agent-vfc/openniw-jobs-b74de3/vuln.py @@ -0,0 +1,23 @@ +# Provenance: ziyuzuo/openniw (vuln). +# repo: ziyuzuo/openniw +# commit: 98a0c392ba96cd2c4aa746dd6fc2b183f385225e +# parent: 98a0c392ba96cd2c4aa746dd6fc2b183f385225e +# commit_url: https://github.com/ziyuzuo/openniw/commit/b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68 +# cve: +# license: MIT +# function: get_job +# relpath: backend/app/routers/jobs.py +# provenance: agent +# mechanism: identity_from_request_body + +async def get_job(job_id: str, user: dict = Depends(auth.current_user)) -> dict: + job = await jobs_service.get(job_id) + if job is None: + raise HTTPException(404, "Job not found") + return { + "id": str(job["id"]), + "kind": job["kind"], + "status": job["status"], + "result": job["result"], + "error": job["error"], + } diff --git a/benchmarks/pairs/agent-vfc/overleaf-with-claude-exportscontroller-3980b9/fixed.mjs b/benchmarks/pairs/agent-vfc/overleaf-with-claude-exportscontroller-3980b9/fixed.mjs new file mode 100644 index 0000000..bb8e2cb --- /dev/null +++ b/benchmarks/pairs/agent-vfc/overleaf-with-claude-exportscontroller-3980b9/fixed.mjs @@ -0,0 +1,82 @@ +// Provenance: ZUENS2020/overleaf-with-claude (fixed). +// repo: ZUENS2020/overleaf-with-claude +// commit: 3980b9e5806defb63e45ae2696e6c1899f3a8431 +// parent: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed +// commit_url: https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431 +// cve: +// license: AGPL-3.0 +// function: exportProject +// relpath: services/web/app/src/Features/Exports/ExportsController.mjs +// provenance: agent +// mechanism: identity_from_request_body + +import { expressify } from '@overleaf/promise-utils' +import SessionManager from '../Authentication/SessionManager.mjs' +import logger from '@overleaf/logger' +import OError from '@overleaf/o-error' +import settings from '@overleaf/settings' + +async function exportProject(req, res, next) { + const { project_id: projectId, brand_variation_id: brandVariationId } = + req.params + const userId = SessionManager.getLoggedInUserId(req.session) + const exportParams = { + project_id: projectId, + brand_variation_id: brandVariationId, + user_id: userId, + } + + if (req.body) { + if (req.body.firstName) { + exportParams.first_name = req.body.firstName.trim() + } + if (req.body.lastName) { + exportParams.last_name = req.body.lastName.trim() + } + // additional parameters for gallery exports + if (req.body.title) { + exportParams.title = req.body.title.trim() + } + if (req.body.description) { + exportParams.description = req.body.description.trim() + } + if (req.body.author) { + exportParams.author = req.body.author.trim() + } + if (req.body.license) { + exportParams.license = req.body.license.trim() + } + if (req.body.showSource != null) { + exportParams.show_source = req.body.showSource + } + } + + try { + const exportData = await ExportsHandler.exportProject(exportParams) + logger.debug( + { + userId, + projectId, + brandVariationId, + exportV1Id: exportData.v1_id, + }, + 'exported project' + ) + return res.json({ + export_v1_id: exportData.v1_id, + token: exportData.token, + message: exportData.message, + }) + } catch (err) { + const info = OError.getFullInfo(err) + if (info?.forwardResponse) { + logger.debug( + { responseError: info.forwardResponse }, + 'forwarding response' + ) + const statusCode = info.forwardResponse.status || 500 + return res.status(statusCode).json(info.forwardResponse) + } + throw err + } +} diff --git a/benchmarks/pairs/agent-vfc/overleaf-with-claude-exportscontroller-3980b9/vuln.mjs b/benchmarks/pairs/agent-vfc/overleaf-with-claude-exportscontroller-3980b9/vuln.mjs new file mode 100644 index 0000000..45151dc --- /dev/null +++ b/benchmarks/pairs/agent-vfc/overleaf-with-claude-exportscontroller-3980b9/vuln.mjs @@ -0,0 +1,80 @@ +// Provenance: ZUENS2020/overleaf-with-claude (vuln). +// repo: ZUENS2020/overleaf-with-claude +// commit: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed +// parent: 5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed +// commit_url: https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431 +// cve: +// license: AGPL-3.0 +// function: exportProject +// relpath: services/web/app/src/Features/Exports/ExportsController.mjs +// provenance: agent +// mechanism: identity_from_request_body + +import { expressify } from '@overleaf/promise-utils' +import SessionManager from '../Authentication/SessionManager.mjs' +import logger from '@overleaf/logger' +import OError from '@overleaf/o-error' + +async function exportProject(req, res, next) { + const { project_id: projectId, brand_variation_id: brandVariationId } = + req.params + const userId = SessionManager.getLoggedInUserId(req.session) + const exportParams = { + project_id: projectId, + brand_variation_id: brandVariationId, + user_id: userId, + } + + if (req.body) { + if (req.body.firstName) { + exportParams.first_name = req.body.firstName.trim() + } + if (req.body.lastName) { + exportParams.last_name = req.body.lastName.trim() + } + // additional parameters for gallery exports + if (req.body.title) { + exportParams.title = req.body.title.trim() + } + if (req.body.description) { + exportParams.description = req.body.description.trim() + } + if (req.body.author) { + exportParams.author = req.body.author.trim() + } + if (req.body.license) { + exportParams.license = req.body.license.trim() + } + if (req.body.showSource != null) { + exportParams.show_source = req.body.showSource + } + } + + try { + const exportData = await ExportsHandler.exportProject(exportParams) + logger.debug( + { + userId, + projectId, + brandVariationId, + exportV1Id: exportData.v1_id, + }, + 'exported project' + ) + return res.json({ + export_v1_id: exportData.v1_id, + message: exportData.message, + }) + } catch (err) { + const info = OError.getFullInfo(err) + if (info?.forwardResponse) { + logger.debug( + { responseError: info.forwardResponse }, + 'forwarding response' + ) + const statusCode = info.forwardResponse.status || 500 + return res.status(statusCode).json(info.forwardResponse) + } + throw err + } +} diff --git a/benchmarks/pairs/agent-vfc/popmark-editorconfig-e5ac40/fixed.ts b/benchmarks/pairs/agent-vfc/popmark-editorconfig-e5ac40/fixed.ts new file mode 100644 index 0000000..d954609 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/popmark-editorconfig-e5ac40/fixed.ts @@ -0,0 +1,33 @@ +// Provenance: dayflower/popmark (fixed). +// repo: dayflower/popmark +// commit: e5ac407cef37c37178d440541ede301c838640fb +// parent: 31d1ac79b4cfa3118597348e79800a6561115149 +// commit_url: https://github.com/dayflower/popmark/commit/e5ac407cef37c37178d440541ede301c838640fb +// cve: +// license: MIT +// function: deepMergeInto +// relpath: src/lexical-markdown/config/editorConfig.ts +// provenance: agent +// mechanism: prototype_pollution + +function deepMergeInto( + target: Record, + source: Record, +): Record { + for (const key of Object.keys(source)) { + // popmark patch: guard against prototype pollution (CodeQL). + if (key === "__proto__" || key === "constructor" || key === "prototype") { + continue; + } + const sourceValue = source[key]; + if (sourceValue === undefined) continue; + if (isPlainObject(sourceValue)) { + const targetValue = target[key]; + const nested = isPlainObject(targetValue) ? targetValue : {}; + target[key] = deepMergeInto(nested, sourceValue); + } else { + target[key] = sourceValue; + } + } + return target; +} diff --git a/benchmarks/pairs/agent-vfc/popmark-editorconfig-e5ac40/vuln.ts b/benchmarks/pairs/agent-vfc/popmark-editorconfig-e5ac40/vuln.ts new file mode 100644 index 0000000..0ad5956 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/popmark-editorconfig-e5ac40/vuln.ts @@ -0,0 +1,29 @@ +// Provenance: dayflower/popmark (vuln). +// repo: dayflower/popmark +// commit: 31d1ac79b4cfa3118597348e79800a6561115149 +// parent: 31d1ac79b4cfa3118597348e79800a6561115149 +// commit_url: https://github.com/dayflower/popmark/commit/e5ac407cef37c37178d440541ede301c838640fb +// cve: +// license: MIT +// function: deepMergeInto +// relpath: src/lexical-markdown/config/editorConfig.ts +// provenance: agent +// mechanism: prototype_pollution + +function deepMergeInto( + target: Record, + source: Record, +): Record { + for (const key of Object.keys(source)) { + const sourceValue = source[key]; + if (sourceValue === undefined) continue; + if (isPlainObject(sourceValue)) { + const targetValue = target[key]; + const nested = isPlainObject(targetValue) ? targetValue : {}; + target[key] = deepMergeInto(nested, sourceValue); + } else { + target[key] = sourceValue; + } + } + return target; +} diff --git a/benchmarks/pairs/agent-vfc/rag-pipeline-utils-config-handler-76e5f0/fixed.js b/benchmarks/pairs/agent-vfc/rag-pipeline-utils-config-handler-76e5f0/fixed.js new file mode 100644 index 0000000..9edabcd --- /dev/null +++ b/benchmarks/pairs/agent-vfc/rag-pipeline-utils-config-handler-76e5f0/fixed.js @@ -0,0 +1,51 @@ +// Provenance: DevilsDev/rag-pipeline-utils (fixed). +// repo: DevilsDev/rag-pipeline-utils +// commit: 76e5f0a94d6803d0e463d26a61cedb7da90857a7 +// parent: ba587867118f703632ff2dc63c4feb8ad6793420 +// commit_url: https://github.com/DevilsDev/rag-pipeline-utils/commit/76e5f0a94d6803d0e463d26a61cedb7da90857a7 +// cve: +// license: MIT +// function: handleConfigSet +// relpath: src/cli/handlers/config-handler.js +// provenance: agent +// mechanism: prototype_pollution + +async function handleConfigSet(globalOptions, key, value) { + try { + const config = JSON.parse(await fs.readFile(globalOptions.config, "utf-8")); + + // Set value using dot notation + const keys = key.split("."); + const forbidden = new Set(["__proto__", "constructor", "prototype"]); + if (keys.some((k) => forbidden.has(k))) { + logger.error("Invalid configuration key: contains restricted property"); + process.exit(1); + } + + let current = config; + + for (let i = 0; i < keys.length - 1; i++) { + if (!current[keys[i]]) { + current[keys[i]] = {}; + } + current = current[keys[i]]; + } + + // Parse value + let parsedValue; + try { + parsedValue = JSON.parse(value); + } catch (error) { + parsedValue = value; // Keep as string + } + + current[keys[keys.length - 1]] = parsedValue; + + // Save configuration + await fs.writeFile(globalOptions.config, JSON.stringify(config, null, 2)); + console.log("✅ Configuration updated"); + } catch (error) { + logger.error("❌ Failed to set configuration:", error.message); + process.exit(1); + } +} diff --git a/benchmarks/pairs/agent-vfc/rag-pipeline-utils-config-handler-76e5f0/vuln.js b/benchmarks/pairs/agent-vfc/rag-pipeline-utils-config-handler-76e5f0/vuln.js new file mode 100644 index 0000000..639f6eb --- /dev/null +++ b/benchmarks/pairs/agent-vfc/rag-pipeline-utils-config-handler-76e5f0/vuln.js @@ -0,0 +1,45 @@ +// Provenance: DevilsDev/rag-pipeline-utils (vuln). +// repo: DevilsDev/rag-pipeline-utils +// commit: ba587867118f703632ff2dc63c4feb8ad6793420 +// parent: ba587867118f703632ff2dc63c4feb8ad6793420 +// commit_url: https://github.com/DevilsDev/rag-pipeline-utils/commit/76e5f0a94d6803d0e463d26a61cedb7da90857a7 +// cve: +// license: MIT +// function: handleConfigSet +// relpath: src/cli/handlers/config-handler.js +// provenance: agent +// mechanism: prototype_pollution + +async function handleConfigSet(globalOptions, key, value) { + try { + const config = JSON.parse(await fs.readFile(globalOptions.config, "utf-8")); + + // Set value using dot notation + const keys = key.split("."); + let current = config; + + for (let i = 0; i < keys.length - 1; i++) { + if (!current[keys[i]]) { + current[keys[i]] = {}; + } + current = current[keys[i]]; + } + + // Parse value + let parsedValue; + try { + parsedValue = JSON.parse(value); + } catch (error) { + parsedValue = value; // Keep as string + } + + current[keys[keys.length - 1]] = parsedValue; + + // Save configuration + await fs.writeFile(globalOptions.config, JSON.stringify(config, null, 2)); + console.log("✅ Configuration updated"); + } catch (error) { + logger.error("❌ Failed to set configuration:", error.message); + process.exit(1); + } +} diff --git a/benchmarks/pairs/agent-vfc/recipes.toml b/benchmarks/pairs/agent-vfc/recipes.toml new file mode 100644 index 0000000..dc20296 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/recipes.toml @@ -0,0 +1,977 @@ +# agent-vfc candidate recipes from agent-trailer security-fix commits (build_recipes.py). +# reviewer = "pending" means a human has not yet confirmed the vulnerability and mechanism. + +[[recipe]] +name = "academic-based-qoder-app-a79ddb" +host = "github" +repo = "1173591564/academic-based-qoder" +parent = "8170f0377fe64d1ec2e35769d4ce3d0634d97cef" +commit = "a79ddb90def54e91f7507cbf6b1548b224d642aa" +path = "services/proxy-hub/backend/proxy_hub/app.py" +mode = "hunk" +language = "python" +relpath = "services/proxy-hub/backend/proxy_hub/app.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "academic-based-qoder" +trailer = "Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>" +reviewer = "pending" +evidence = "fix(hub): allow DSH token validation CORS" +commit_url = "https://github.com/1173591564/academic-based-qoder/commit/a79ddb90def54e91f7507cbf6b1548b224d642aa" +split = "train" + +[[recipe]] +name = "anastasios-pw-session-14ec1a" +host = "github" +repo = "Anas329796/Anastasios" +parent = "adb7b0d5d6b81c46d56451c709e7eac9d0dc93fb" +commit = "14ec1ac50f9e4e255d875236d30129c6207401d4" +path = "extensions/browser/src/browser/pw-session.ts" +mode = "hunk" +language = "typescript" +relpath = "extensions/browser/src/browser/pw-session.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-918" +class = "server-side request forgery" +mechanism = "source_reaches_sink" +provenance = "agent" +origin = "github-agent-trailer" +project = "Anastasios" +trailer = "Co-authored-by: Devin Robison " +reviewer = "pending" +evidence = "fix(browser): harden SSRF redirect guard against non-navigation document hops [AI] (#62355)" +commit_url = "https://github.com/Anas329796/Anastasios/commit/14ec1ac50f9e4e255d875236d30129c6207401d4" +split = "holdout" + +[[recipe]] +name = "arsnova-eu-httpcors-9bcba4" +host = "github" +repo = "kqc-real/arsnova.eu" +parent = "2ae60c434f3fb80f91807a2ec485ee1a7ec76bd2" +commit = "9bcba4f442aecc6a5e16ef2dc6115177cfc2b6df" +path = "apps/backend/src/lib/httpCors.ts" +mode = "hunk" +language = "typescript" +relpath = "apps/backend/src/lib/httpCors.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "arsnova.eu" +trailer = "Co-authored-by: Cursor " +reviewer = "pending" +evidence = "fix(security): Produktions-CORS auf Same-Origin begrenzen (#143)" +commit_url = "https://github.com/kqc-real/arsnova.eu/commit/9bcba4f442aecc6a5e16ef2dc6115177cfc2b6df" +split = "train" + +[[recipe]] +name = "astroclaw-pw-session-1553a8" +host = "github" +repo = "P1R4N351/astroclaw" +parent = "d3eed2dc01328f21dd404477bb28e2740fa371a4" +commit = "1553a8adf123913cf7f1f1c75be4c4e5cd6c796b" +path = "extensions/browser/src/browser/pw-session.ts" +mode = "hunk" +language = "typescript" +relpath = "extensions/browser/src/browser/pw-session.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-918" +class = "server-side request forgery" +mechanism = "source_reaches_sink" +provenance = "agent" +origin = "github-agent-trailer" +project = "astroclaw" +trailer = "Co-authored-by: Devin Robison " +reviewer = "pending" +evidence = "fix(browser): harden SSRF redirect guard against non-navigation document hops [AI] (#62355)" +commit_url = "https://github.com/P1R4N351/astroclaw/commit/1553a8adf123913cf7f1f1c75be4c4e5cd6c796b" +split = "holdout" + +[[recipe]] +name = "capgo-app-apptabs-7d73fd" +host = "github" +repo = "Cap-go/capgo.app" +parent = "415ea4e19d7da19a3f8b0cd0bba541d60b902522" +commit = "7d73fd21a59eaa3a4e20c6dc0d280e73e47435b7" +path = "src/constants/appTabs.ts" +mode = "hunk" +language = "typescript" +relpath = "src/constants/appTabs.ts" +license = "AGPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "capgo.app" +trailer = "Co-authored-by: Cursor " +reviewer = "pending" +evidence = "fix(notifications): harden API CORS and mark console tab as beta (#2623)" +commit_url = "https://github.com/Cap-go/capgo.app/commit/7d73fd21a59eaa3a4e20c6dc0d280e73e47435b7" +split = "train" + +[[recipe]] +name = "capgo-app-hono-4dadd2" +host = "github" +repo = "Cap-go/capgo.app" +parent = "8c0ac7e233946abd8dc26581b871e9fcd0928899" +commit = "4dadd2580d02213d227a331eee0232aa59859b59" +path = "supabase/functions/_backend/utils/hono.ts" +mode = "hunk" +language = "typescript" +relpath = "supabase/functions/_backend/utils/hono.ts" +license = "AGPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "capgo.app" +trailer = "Co-authored-by: Cursor " +reviewer = "pending" +evidence = "fix(api): CORS for Capacitor localhost WebView (#3105)" +commit_url = "https://github.com/Cap-go/capgo.app/commit/4dadd2580d02213d227a331eee0232aa59859b59" +split = "holdout" + +[[recipe]] +name = "dailyforge-server-ed1470" +host = "github" +repo = "Annamdevula-Nissi-Grace/Dailyforge" +parent = "adc164d6e87ff735a3ea5637a2febfd3217e168a" +commit = "ed1470814a108bd725c8b27bd9d86cf95a979a96" +path = "backend/src/server.js" +mode = "hunk" +language = "javascript" +relpath = "backend/src/server.js" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "Dailyforge" +trailer = "Co-authored-by: Copilot " +reviewer = "pending" +evidence = "Fix: add localhost fallback for CORS origin (#172)" +commit_url = "https://github.com/Annamdevula-Nissi-Grace/Dailyforge/commit/ed1470814a108bd725c8b27bd9d86cf95a979a96" +split = "train" + +[[recipe]] +name = "dnf-plugin-anyrepo-config-26d19a" +host = "github" +repo = "jfut/dnf-plugin-anyrepo" +parent = "33cde479d0db8f45dd89fbdcfb2bc0f1be66bd7a" +commit = "26d19aeb8884f244e34ccfdc4f49f07055adc4d0" +path = "dnf_plugin_anyrepo/config.py" +mode = "hunk" +language = "python" +relpath = "dnf_plugin_anyrepo/config.py" +license = "Apache-2.0" +cve = "" +year = 2026 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "agent" +origin = "github-agent-trailer" +project = "dnf-plugin-anyrepo" +trailer = "Co-authored-by: Codex " +reviewer = "pending" +evidence = "fix(config): prevent path traversal in repository names" +commit_url = "https://github.com/jfut/dnf-plugin-anyrepo/commit/26d19aeb8884f244e34ccfdc4f49f07055adc4d0" +split = "holdout" + +[[recipe]] +name = "edutrace-page-ee7ba2" +host = "github" +repo = "jardimdesoftware/edutrace" +parent = "c4dc6ddcc5492c77c8a5ad7369c87dea29c001fd" +commit = "ee7ba25fb23ea89cc1b67549cb5e32c8ead92739" +path = "front/src/app/editar-anamnese/page.tsx" +mode = "hunk" +language = "typescript" +relpath = "front/src/app/editar-anamnese/page.tsx" +license = "Apache-2.0" +cve = "" +year = 2026 +cwe = "CWE-1321" +class = "prototype pollution" +mechanism = "prototype_pollution" +provenance = "agent" +origin = "github-agent-trailer" +project = "edutrace" +trailer = "Co-Authored-By: Claude Fable 5 " +reviewer = "pending" +evidence = "fix(front): impedir prototype pollution nos handlers de formulário aninhado" +commit_url = "https://github.com/jardimdesoftware/edutrace/commit/ee7ba25fb23ea89cc1b67549cb5e32c8ead92739" +split = "train" + +[[recipe]] +name = "etoj-index-605274" +host = "github" +repo = "Eternity-Sky/_ETOJ" +parent = "b0c020fd9ca0204630fca8a857d6fc1b1497bd5b" +commit = "6052743d477eeff62e32553c2790785d3f52c641" +path = "api/src/index.ts" +mode = "hunk" +language = "typescript" +relpath = "api/src/index.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "_ETOJ" +trailer = "Generated with [Devin](https://devin.ai)" +reviewer = "pending" +evidence = "Fix CORS: Add PATCH method to allowed methods" +commit_url = "https://github.com/Eternity-Sky/_ETOJ/commit/6052743d477eeff62e32553c2790785d3f52c641" +split = "holdout" + +[[recipe]] +name = "etude-lexical-markdown-editorconfig-c61a8b" +host = "github" +repo = "dayflower/etude-lexical-markdown" +parent = "0a2184f59e79a04b1365cd49200026e8f2ffdbd5" +commit = "c61a8bf78fe20d6ae0eab2e4b8b02004508525ed" +path = "src/config/editorConfig.ts" +mode = "hunk" +language = "typescript" +relpath = "src/config/editorConfig.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-1321" +class = "prototype pollution" +mechanism = "prototype_pollution" +provenance = "agent" +origin = "github-agent-trailer" +project = "etude-lexical-markdown" +trailer = "Co-Authored-By: Claude " +reviewer = "pending" +evidence = "fix: guard deepMergeInto against prototype pollution" +commit_url = "https://github.com/dayflower/etude-lexical-markdown/commit/c61a8bf78fe20d6ae0eab2e4b8b02004508525ed" +split = "train" + +[[recipe]] +name = "etude-lexical-markdown-editorconfig-db4f54" +host = "github" +repo = "dayflower/etude-lexical-markdown" +parent = "1c865815d05b072f4973388c3ac66b6696d9efde" +commit = "db4f54b1ab80ad596de6425e84f90aa781c49999" +path = "src/config/editorConfig.ts" +mode = "hunk" +language = "typescript" +relpath = "src/config/editorConfig.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-1321" +class = "prototype pollution" +mechanism = "prototype_pollution" +provenance = "agent" +origin = "github-agent-trailer" +project = "etude-lexical-markdown" +trailer = "Co-Authored-By: Claude " +reviewer = "pending" +evidence = "fix: make prototype pollution guard recognizable by CodeQL" +commit_url = "https://github.com/dayflower/etude-lexical-markdown/commit/db4f54b1ab80ad596de6425e84f90aa781c49999" +split = "holdout" + +[[recipe]] +name = "gitim-git-cors-proxy-e4c3a4" +host = "github" +repo = "CiferaTeam/GitIM" +parent = "8afa34d45e119164abec0b76ed77a56f442749a8" +commit = "e4c3a4794dbe9fbcce679f7df1ecd2b09b40f9d6" +path = "products/gitim/frontend/src/lib/git-cors-proxy.ts" +mode = "hunk" +language = "typescript" +relpath = "products/gitim/frontend/src/lib/git-cors-proxy.ts" +license = "Apache-2.0" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "GitIM" +trailer = "Co-authored-by: Codex " +reviewer = "pending" +evidence = "fix(frontend): use gitim cors proxy by default" +commit_url = "https://github.com/CiferaTeam/GitIM/commit/e4c3a4794dbe9fbcce679f7df1ecd2b09b40f9d6" +split = "train" + +[[recipe]] +name = "lensops-kubernetesmanagement-fe4910" +host = "github" +repo = "amitknigam84/lensops" +parent = "9b87ecc3a44a7fe2a5185cca1fe52441647d5748" +commit = "fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84" +path = "components/KubernetesManagement.tsx" +mode = "hunk" +language = "typescript" +relpath = "components/KubernetesManagement.tsx" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-862" +class = "missing authorization" +mechanism = "missing_auth_guard" +provenance = "agent" +origin = "github-agent-trailer" +project = "lensops" +trailer = "Co-Authored-By: Claude Sonnet 4.6 " +reviewer = "pending" +evidence = "Fix missing auth header on namespaces API call" +commit_url = "https://github.com/amitknigam84/lensops/commit/fe49109aedfe21dea4d24d4d9f2fa3f1ecfb6c84" +split = "holdout" + +[[recipe]] +name = "market-cap-route-4f8ae9" +host = "github" +repo = "navaneethbv/market-cap" +parent = "fec6f024ce72c9c47b61cbac6ce7a35cb2280602" +commit = "4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6" +path = "app/auth/confirm/route.ts" +mode = "hunk" +language = "typescript" +relpath = "app/auth/confirm/route.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-601" +class = "open redirect" +mechanism = "source_reaches_sink" +provenance = "agent" +origin = "github-agent-trailer" +project = "market-cap" +trailer = "Co-Authored-By: Claude Fable 5 " +reviewer = "pending" +evidence = "Fix open redirects on the auth next param" +commit_url = "https://github.com/navaneethbv/market-cap/commit/4f8ae9be5519ced5a975f1190e3e4bc43f84e4d6" +split = "train" + +[[recipe]] +name = "melodix-playerprovider-089931" +host = "github" +repo = "alexshao3/melodix" +parent = "c074fc7413338aefcbea3170ef4d3ba486a8a4b7" +commit = "089931ef71ea1e5b7586520baf1a5498e2f05b13" +path = "apps/miniapp/src/components/PlayerProvider.tsx" +mode = "hunk" +language = "typescript" +relpath = "apps/miniapp/src/components/PlayerProvider.tsx" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "melodix" +trailer = "Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>" +reviewer = "pending" +evidence = "Merge pull request #17 from alexshao3/devin/1777216143-fix-audio-cors" +commit_url = "https://github.com/alexshao3/melodix/commit/089931ef71ea1e5b7586520baf1a5498e2f05b13" +split = "holdout" + +[[recipe]] +name = "mercado-do-vale-news-catalogmetadataservice-9b5fcf" +host = "github" +repo = "handielson/Mercado-do-vale-news" +parent = "70ca282b819ac1d60f4b5e665d224b1043ff1ccc" +commit = "9b5fcf2dae41e56f5f97952266f2d6ba59598598" +path = "services/catalogMetadataService.ts" +mode = "hunk" +language = "typescript" +relpath = "services/catalogMetadataService.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "Mercado-do-vale-news" +trailer = "Co-Authored-By: Codex " +reviewer = "pending" +evidence = "fix(catalog): avoid metadata cors preflight" +commit_url = "https://github.com/handielson/Mercado-do-vale-news/commit/9b5fcf2dae41e56f5f97952266f2d6ba59598598" +split = "train" + +[[recipe]] +name = "money-track-category-icons-4d4a5b" +host = "github" +repo = "andrebubniak/money-track" +parent = "add96df6d6a6b61506635c23dbacf6c6cfe24dc2" +commit = "4d4a5b063278c8b6697c3faae133d7c39fcd01c0" +path = "src/lib/category-icons.ts" +mode = "hunk" +language = "typescript" +relpath = "src/lib/category-icons.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-1321" +class = "prototype pollution" +mechanism = "prototype_pollution" +provenance = "agent" +origin = "github-agent-trailer" +project = "money-track" +trailer = "Co-Authored-By: Claude Sonnet 5 " +reviewer = "pending" +evidence = "fix(category-icons): use Object.hasOwn to prevent prototype pollution in type guard" +commit_url = "https://github.com/andrebubniak/money-track/commit/4d4a5b063278c8b6697c3faae133d7c39fcd01c0" +split = "holdout" + +[[recipe]] +name = "nestjs-filter-normalizer-aa0e60" +host = "github" +repo = "DavideCarvalho/nestjs-filter" +parent = "d899b40d3466f2ffbe9f01c4d49267412401f078" +commit = "aa0e600d05997ea3d77538ea1c2db427c439f2e8" +path = "packages/core/src/input/normalizer.ts" +mode = "hunk" +language = "typescript" +relpath = "packages/core/src/input/normalizer.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-1321" +class = "prototype pollution" +mechanism = "prototype_pollution" +provenance = "agent" +origin = "github-agent-trailer" +project = "nestjs-filter" +trailer = "Co-Authored-By: Claude Opus 4.7 " +reviewer = "pending" +evidence = "fix(core): add prototype pollution guard to normalizeInput" +commit_url = "https://github.com/DavideCarvalho/nestjs-filter/commit/aa0e600d05997ea3d77538ea1c2db427c439f2e8" +split = "train" + +[[recipe]] +name = "new-erp-final-app-6e9644" +host = "github" +repo = "Sachin-Salgar/NEW_ERP_FINAL" +parent = "08976c5d7a07b0663669b875b4507446199f85d7" +commit = "6e9644c81ce3fb356669157182a5ef09a70c4b59" +path = "src/presentation/http/app.ts" +mode = "hunk" +language = "typescript" +relpath = "src/presentation/http/app.ts" +license = "Apache-2.0" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "NEW_ERP_FINAL" +trailer = "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +reviewer = "pending" +evidence = "fix: allow patch requests through cors" +commit_url = "https://github.com/Sachin-Salgar/NEW_ERP_FINAL/commit/6e9644c81ce3fb356669157182a5ef09a70c4b59" +split = "holdout" + +[[recipe]] +name = "nimestream-route-d17f53" +host = "github" +repo = "Irnhakim/nimestream" +parent = "0c37bfd9e5292b15b1d7af51f52793d9bdf1cef0" +commit = "d17f535bf3012041b056ff3aa991ff6cfa198e43" +path = "app/api/img/route.js" +mode = "hunk" +language = "javascript" +relpath = "app/api/img/route.js" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-918" +class = "server-side request forgery" +mechanism = "source_reaches_sink" +provenance = "agent" +origin = "github-agent-trailer" +project = "nimestream" +trailer = "Co-Authored-By: Claude " +reviewer = "pending" +evidence = "fix: resolve command injection and SSRF/Open Redirect security issues in multiScraper and image proxy API" +commit_url = "https://github.com/Irnhakim/nimestream/commit/d17f535bf3012041b056ff3aa991ff6cfa198e43" +split = "train" + +[[recipe]] +name = "onlook-helper-57ce1c" +host = "github" +repo = "zhcndoc/onlook" +parent = "cb97e012729eca441e09b1503282cb4bc2f4bcfb" +commit = "57ce1ccfacc7e8484cae59013a2b1e34518560e5" +path = "apps/web/client/src/server/api/routers/project/helper.ts" +mode = "hunk" +language = "typescript" +relpath = "apps/web/client/src/server/api/routers/project/helper.ts" +license = "Apache-2.0" +cve = "" +year = 2025 +cwe = "CWE-862" +class = "missing authorization" +mechanism = "missing_auth_guard" +provenance = "agent" +origin = "github-agent-trailer" +project = "onlook" +trailer = "Co-Authored-By: Claude " +reviewer = "pending" +evidence = "Fix CVE-2025-63783: Add authorization checks to project mutation APIs (#3062)" +commit_url = "https://github.com/zhcndoc/onlook/commit/57ce1ccfacc7e8484cae59013a2b1e34518560e5" +split = "holdout" + +[[recipe]] +name = "openniw-jobs-0fc947" +host = "github" +repo = "HHHHHejia/openniw" +parent = "4d91ed82fa9a5ec6bf5ef96da1d221fc187700dc" +commit = "0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10" +path = "backend/app/routers/jobs.py" +mode = "hunk" +language = "python" +relpath = "backend/app/routers/jobs.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "insecure direct object reference" +mechanism = "identity_from_request_body" +provenance = "agent" +origin = "github-agent-trailer" +project = "openniw" +trailer = "Co-Authored-By: Claude Fable 5 " +reviewer = "pending" +evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership" +commit_url = "https://github.com/HHHHHejia/openniw/commit/0fc94733f6ca3c6b129a0a8e6d7b3acce19d5a10" +split = "train" + +[[recipe]] +name = "openniw-jobs-b74de3" +host = "github" +repo = "ziyuzuo/openniw" +parent = "98a0c392ba96cd2c4aa746dd6fc2b183f385225e" +commit = "b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68" +path = "backend/app/routers/jobs.py" +mode = "hunk" +language = "python" +relpath = "backend/app/routers/jobs.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "insecure direct object reference" +mechanism = "identity_from_request_body" +provenance = "agent" +origin = "github-agent-trailer" +project = "openniw" +trailer = "Co-Authored-By: Claude Fable 5 " +reviewer = "pending" +evidence = "Fix IDOR on job endpoint: case-bound jobs require case ownership" +commit_url = "https://github.com/ziyuzuo/openniw/commit/b74de31b0a5f37e4e929ba6aa74dbe0f5e6d9a68" +split = "holdout" + +[[recipe]] +name = "overleaf-with-claude-exportscontroller-3980b9" +host = "github" +repo = "ZUENS2020/overleaf-with-claude" +parent = "5a886aa9fb9fe4e2203f3cb1d62c91f4be0525ed" +commit = "3980b9e5806defb63e45ae2696e6c1899f3a8431" +path = "services/web/app/src/Features/Exports/ExportsController.mjs" +mode = "hunk" +language = "javascript" +relpath = "services/web/app/src/Features/Exports/ExportsController.mjs" +license = "AGPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "insecure direct object reference" +mechanism = "identity_from_request_body" +provenance = "agent" +origin = "github-agent-trailer" +project = "overleaf-with-claude" +trailer = "Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>" +reviewer = "pending" +evidence = "Fix IDOR in exports by adding token verification (Issue #31637) (#32883)" +commit_url = "https://github.com/ZUENS2020/overleaf-with-claude/commit/3980b9e5806defb63e45ae2696e6c1899f3a8431" +split = "train" + +[[recipe]] +name = "popmark-editorconfig-e5ac40" +host = "github" +repo = "dayflower/popmark" +parent = "31d1ac79b4cfa3118597348e79800a6561115149" +commit = "e5ac407cef37c37178d440541ede301c838640fb" +path = "src/lexical-markdown/config/editorConfig.ts" +mode = "hunk" +language = "typescript" +relpath = "src/lexical-markdown/config/editorConfig.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-1321" +class = "prototype pollution" +mechanism = "prototype_pollution" +provenance = "agent" +origin = "github-agent-trailer" +project = "popmark" +trailer = "Co-Authored-By: Claude " +reviewer = "pending" +evidence = "fix: guard deepMergeInto against prototype pollution" +commit_url = "https://github.com/dayflower/popmark/commit/e5ac407cef37c37178d440541ede301c838640fb" +split = "holdout" + +[[recipe]] +name = "rag-pipeline-utils-config-handler-76e5f0" +host = "github" +repo = "DevilsDev/rag-pipeline-utils" +parent = "ba587867118f703632ff2dc63c4feb8ad6793420" +commit = "76e5f0a94d6803d0e463d26a61cedb7da90857a7" +path = "src/cli/handlers/config-handler.js" +mode = "hunk" +language = "javascript" +relpath = "src/cli/handlers/config-handler.js" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-1321" +class = "prototype pollution" +mechanism = "prototype_pollution" +provenance = "agent" +origin = "github-agent-trailer" +project = "rag-pipeline-utils" +trailer = "Co-Authored-By: Claude Opus 4.6 (1M context) " +reviewer = "pending" +evidence = "fix(security): prevent prototype pollution in config-handler" +commit_url = "https://github.com/DevilsDev/rag-pipeline-utils/commit/76e5f0a94d6803d0e463d26a61cedb7da90857a7" +split = "train" + +[[recipe]] +name = "registry-proxy-worker-426847" +host = "github" +repo = "agents-repo/registry-proxy" +parent = "6d5937972510cdb5ebca30607b538fdc2e280094" +commit = "426847899eb2ee94ae4e5a3aacc50423e36d2dc8" +path = "src/worker.js" +mode = "hunk" +language = "javascript" +relpath = "src/worker.js" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "registry-proxy" +trailer = "Co-authored-by: Cursor " +reviewer = "pending" +evidence = "fix: handle CORS OPTIONS preflight for catalog fetches (#10)" +commit_url = "https://github.com/agents-repo/registry-proxy/commit/426847899eb2ee94ae4e5a3aacc50423e36d2dc8" +split = "holdout" + +[[recipe]] +name = "rls-alembic-rls-d2b6af" +host = "github" +repo = "DelfinaCare/rls" +parent = "670123485f1c657e9faeb18e633339b6a79e696f" +commit = "d2b6af47347fb285b7a3820a7f2d5dded20907ec" +path = "rls/alembic_rls.py" +mode = "hunk" +language = "python" +relpath = "rls/alembic_rls.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "rls" +trailer = "Co-authored-by: aebrahim <1380168+aebrahim@users.noreply.github.com>" +reviewer = "pending" +evidence = "Guard against ENABLE ROW LEVEL SECURITY on tables with no policies (#143)" +commit_url = "https://github.com/DelfinaCare/rls/commit/d2b6af47347fb285b7a3820a7f2d5dded20907ec" +split = "train" + +[[recipe]] +name = "supplieradvisor-mvp-route-387bcf" +host = "github" +repo = "bigfivefoods/supplieradvisor-mvp" +parent = "a7a9bc260d649ea2e582c07435bfd7d93100647b" +commit = "387bcf40e984a2ee1832ab7beabf0284c5b627d8" +path = "app/api/customers/claims/route.ts" +mode = "hunk" +language = "typescript" +relpath = "app/api/customers/claims/route.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "insecure direct object reference" +mechanism = "identity_from_request_body" +provenance = "agent" +origin = "github-agent-trailer" +project = "supplieradvisor-mvp" +trailer = "Co-authored-by: bigfivefoods <265166842+bigfivefoods@users.noreply.github.com>" +reviewer = "pending" +evidence = "Brief 62: Fix IDOR on customer_claims PATCH/DELETE (#156)" +commit_url = "https://github.com/bigfivefoods/supplieradvisor-mvp/commit/387bcf40e984a2ee1832ab7beabf0284c5b627d8" +split = "holdout" + +[[recipe]] +name = "supplieradvisor-mvp-route-a69e8f" +host = "github" +repo = "bigfivefoods/supplieradvisor-mvp" +parent = "8558538471e4fb10856f8d9a17d2be5ab5c4fb9c" +commit = "a69e8f27c0f4d402573d16f0612b51155bbc46e1" +path = "app/api/customers/opportunities/route.ts" +mode = "hunk" +language = "typescript" +relpath = "app/api/customers/opportunities/route.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "insecure direct object reference" +mechanism = "identity_from_request_body" +provenance = "agent" +origin = "github-agent-trailer" +project = "supplieradvisor-mvp" +trailer = "Co-authored-by: bigfivefoods <265166842+bigfivefoods@users.noreply.github.com>" +reviewer = "pending" +evidence = "Brief 60: Fix IDOR on opportunities GET/PATCH/DELETE (missing company membership checks) (#148)" +commit_url = "https://github.com/bigfivefoods/supplieradvisor-mvp/commit/a69e8f27c0f4d402573d16f0612b51155bbc46e1" +split = "train" + +[[recipe]] +name = "tools-cli-tools-a96bc0" +host = "github" +repo = "D-sorganization/Tools" +parent = "e735d28b82dd1fbc42c0ae01157b38d1b4c7c864" +commit = "a96bc0502a3dcdac303e4f0152508886e5c35c0a" +path = "src/shared/python/ai/tools/cli_tools.py" +mode = "hunk" +language = "python" +relpath = "src/shared/python/ai/tools/cli_tools.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "agent" +origin = "github-agent-trailer" +project = "Tools" +trailer = "Co-authored-by: dieterolson <198168927+dieterolson@users.noreply.github.com>" +reviewer = "pending" +evidence = "🛡️ Sentinel: [CRITICAL] Fix command injection in ShellTool (#3307)" +commit_url = "https://github.com/D-sorganization/Tools/commit/a96bc0502a3dcdac303e4f0152508886e5c35c0a" +split = "holdout" + +[[recipe]] +name = "tools-wsgi-e18686" +host = "github" +repo = "D-sorganization/Tools" +parent = "f34c44a0842ac983e2a984fe7fd985be3ccaf747" +commit = "e18686aaa174a8a4843c07b4727771c92e0f179e" +path = "src/web_applications/unit_converter/wsgi.py" +mode = "hunk" +language = "python" +relpath = "src/web_applications/unit_converter/wsgi.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "agent" +origin = "github-agent-trailer" +project = "Tools" +trailer = "Co-Authored-By: Claude Sonnet 4.6 " +reviewer = "pending" +evidence = "fix(security): validate mesh filenames against path traversal in _download_meshes (#2077) (#2107)" +commit_url = "https://github.com/D-sorganization/Tools/commit/e18686aaa174a8a4843c07b4727771c92e0f179e" +split = "train" + +[[recipe]] +name = "trackly-cli-path-diagnostics-1c1aed" +host = "github" +repo = "trackly-app/trackly-cli" +parent = "8d240ad8d7fc114c8f941ad956a2fdf32391ff60" +commit = "1c1aed89b07edb04051830c8f086e02b65e961c6" +path = "lib/path-diagnostics.js" +mode = "hunk" +language = "javascript" +relpath = "lib/path-diagnostics.js" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "agent" +origin = "github-agent-trailer" +project = "trackly-cli" +trailer = "Co-authored-by: Codex " +reviewer = "pending" +evidence = "fix(apply): report blocked path traversal" +commit_url = "https://github.com/trackly-app/trackly-cli/commit/1c1aed89b07edb04051830c8f086e02b65e961c6" +split = "holdout" + +[[recipe]] +name = "upstreamdrift-local-server-c3a078" +host = "github" +repo = "D-sorganization/UpstreamDrift" +parent = "e28d1da5e103d93951ef11b78a6e9b89ac1fa2bc" +commit = "c3a0787080489b30e2ed0abfac03b51e45da184b" +path = "src/api/local_server.py" +mode = "hunk" +language = "python" +relpath = "src/api/local_server.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "agent" +origin = "github-agent-trailer" +project = "UpstreamDrift" +trailer = "Co-Authored-By: Claude Opus 4.7 (1M context) " +reviewer = "pending" +evidence = "fix(api): harden local server path traversal (#2805) (#2839)" +commit_url = "https://github.com/D-sorganization/UpstreamDrift/commit/c3a0787080489b30e2ed0abfac03b51e45da184b" +split = "train" + +[[recipe]] +name = "vantalk-firebase-16c3b0" +host = "github" +repo = "NiSeullent/VanTalk" +parent = "7d442bdf537df96c756d47cfc6ce0ccdb3cd8ea7" +commit = "16c3b02875b1f51c3cac1dc185e199978a35985c" +path = "web/src/firebase.ts" +mode = "hunk" +language = "typescript" +relpath = "web/src/firebase.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-798" +class = "hardcoded credentials" +mechanism = "secret_in_client" +provenance = "agent" +origin = "github-agent-trailer" +project = "VanTalk" +trailer = "Co-authored-by: Cursor " +reviewer = "pending" +evidence = "fix(security): load Firebase API key from env, remove hardcoded secret" +commit_url = "https://github.com/NiSeullent/VanTalk/commit/16c3b02875b1f51c3cac1dc185e199978a35985c" +split = "holdout" + +[[recipe]] +name = "waggledance-swarm-ssrf-host-guard-9a4548" +host = "github" +repo = "Ahkeratmehilaiset/waggledance-swarm" +parent = "934b939f1cca5259eaf807db3319eb97b90d8d80" +commit = "9a454843aa35332ff2eb3106023eee0c4b5e444b" +path = "waggledance/core/v3_13_0/ssrf_host_guard.py" +mode = "hunk" +language = "python" +relpath = "waggledance/core/v3_13_0/ssrf_host_guard.py" +license = "Apache-2.0" +cve = "" +year = 2026 +cwe = "CWE-918" +class = "server-side request forgery" +mechanism = "source_reaches_sink" +provenance = "agent" +origin = "github-agent-trailer" +project = "waggledance-swarm" +trailer = "Co-authored-by: codex-lead-1 " +reviewer = "pending" +evidence = "fix(ssrf): reject bare-string allowed hosts (#1460)" +commit_url = "https://github.com/Ahkeratmehilaiset/waggledance-swarm/commit/9a454843aa35332ff2eb3106023eee0c4b5e444b" +split = "train" + +[[recipe]] +name = "workers-research-utils-209444" +host = "github" +repo = "G4brym/workers-research" +parent = "eab923ad3099c01b002fbd6be7cf994614bbdee4" +commit = "209444eb61cc564c2d6b43267256f85fe3e5e30e" +path = "src/utils.ts" +mode = "hunk" +language = "typescript" +relpath = "src/utils.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql injection" +mechanism = "source_reaches_sink" +provenance = "agent" +origin = "github-agent-trailer" +project = "workers-research" +trailer = "Co-Authored-By: Claude Opus 4.6 " +reviewer = "pending" +evidence = "Fix SQL injection in search by using parameterized queries (#41)" +commit_url = "https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e" +split = "holdout" + +[[recipe]] +name = "zoonk-origin-bfbf67" +host = "github" +repo = "zoonk/zoonk" +parent = "2604269350bff51f58572c133b45010cb0976a5a" +commit = "bfbf670ccb41d63c407d32c51bd26e626a174f31" +path = "packages/utils/src/origin.ts" +mode = "hunk" +language = "typescript" +relpath = "packages/utils/src/origin.ts" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "permissive default" +mechanism = "permissive_default" +provenance = "agent" +origin = "github-agent-trailer" +project = "zoonk" +trailer = "Co-authored-by: Codex " +reviewer = "pending" +evidence = "fix(api): allow staging cors from previews (#1559)" +commit_url = "https://github.com/zoonk/zoonk/commit/bfbf670ccb41d63c407d32c51bd26e626a174f31" +split = "train" diff --git a/benchmarks/pairs/agent-vfc/registry-proxy-worker-426847/fixed.js b/benchmarks/pairs/agent-vfc/registry-proxy-worker-426847/fixed.js new file mode 100644 index 0000000..efa1655 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/registry-proxy-worker-426847/fixed.js @@ -0,0 +1,43 @@ +// Provenance: agents-repo/registry-proxy (fixed). +// repo: agents-repo/registry-proxy +// commit: 426847899eb2ee94ae4e5a3aacc50423e36d2dc8 +// parent: 6d5937972510cdb5ebca30607b538fdc2e280094 +// commit_url: https://github.com/agents-repo/registry-proxy/commit/426847899eb2ee94ae4e5a3aacc50423e36d2dc8 +// cve: +// license: MIT +// function: buildUpstreamRequest +// relpath: src/worker.js +// provenance: agent +// mechanism: permissive_default + +function buildUpstreamRequest(target, env, requestHeaders) { + const headers = new Headers(); + const requestAccept = requestHeaders.get("Accept") || "*/*"; + headers.set("User-Agent", UPSTREAM_USER_AGENT); + + const ifNoneMatch = requestHeaders.get("If-None-Match"); + if (ifNoneMatch) { + headers.set("If-None-Match", ifNoneMatch); + } + + const ifModifiedSince = requestHeaders.get("If-Modified-Since"); + if (ifModifiedSince) { + headers.set("If-Modified-Since", ifModifiedSince); + } + + if (env.GITHUB_TOKEN) { + headers.set("Accept", "application/vnd.github.raw"); + headers.set("Authorization", `Bearer ${env.GITHUB_TOKEN}`); + return { + url: buildContentsApiUrl(target.ref, target.targetPath), + headers, + }; + } + + headers.set("Accept", requestAccept); + + return { + url: buildUpstreamUrl(target.ref, target.targetPath), + headers, + }; +} diff --git a/benchmarks/pairs/agent-vfc/registry-proxy-worker-426847/vuln.js b/benchmarks/pairs/agent-vfc/registry-proxy-worker-426847/vuln.js new file mode 100644 index 0000000..c8b20ab --- /dev/null +++ b/benchmarks/pairs/agent-vfc/registry-proxy-worker-426847/vuln.js @@ -0,0 +1,33 @@ +// Provenance: agents-repo/registry-proxy (vuln). +// repo: agents-repo/registry-proxy +// commit: 6d5937972510cdb5ebca30607b538fdc2e280094 +// parent: 6d5937972510cdb5ebca30607b538fdc2e280094 +// commit_url: https://github.com/agents-repo/registry-proxy/commit/426847899eb2ee94ae4e5a3aacc50423e36d2dc8 +// cve: +// license: MIT +// function: buildUpstreamRequest +// relpath: src/worker.js +// provenance: agent +// mechanism: permissive_default + +function buildUpstreamRequest(target, env, requestHeaders) { + const headers = new Headers(); + const requestAccept = requestHeaders.get("Accept") || "*/*"; + headers.set("User-Agent", UPSTREAM_USER_AGENT); + + if (env.GITHUB_TOKEN) { + headers.set("Accept", "application/vnd.github.raw"); + headers.set("Authorization", `Bearer ${env.GITHUB_TOKEN}`); + return { + url: buildContentsApiUrl(target.ref, target.targetPath), + headers, + }; + } + + headers.set("Accept", requestAccept); + + return { + url: buildUpstreamUrl(target.ref, target.targetPath), + headers, + }; +} diff --git a/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-387bcf/fixed.ts b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-387bcf/fixed.ts new file mode 100644 index 0000000..619dedf --- /dev/null +++ b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-387bcf/fixed.ts @@ -0,0 +1,69 @@ +// Provenance: bigfivefoods/supplieradvisor-mvp (fixed). +// repo: bigfivefoods/supplieradvisor-mvp +// commit: 387bcf40e984a2ee1832ab7beabf0284c5b627d8 +// parent: a7a9bc260d649ea2e582c07435bfd7d93100647b +// commit_url: https://github.com/bigfivefoods/supplieradvisor-mvp/commit/387bcf40e984a2ee1832ab7beabf0284c5b627d8 +// cve: +// license: MIT +// function: PATCH +// relpath: app/api/customers/claims/route.ts +// provenance: agent +// mechanism: identity_from_request_body + +export async function PATCH(request: NextRequest) { + try { + const body = await request.json(); + const claimId = Number(body.id); + const companyId = Number(body.companyId); + if (!Number.isFinite(claimId) || claimId <= 0) { + return NextResponse.json({ error: 'id required' }, { status: 400 }); + } + if (!Number.isFinite(companyId) || companyId <= 0) { + return NextResponse.json({ error: 'companyId required' }, { status: 400 }); + } + + const _gate = await requireCompanyAccess(request, companyId, { legacyPrivyUserId: legacyPrivyFrom(request) }); + if (!_gate.ok) return _gate.response; + + const supabase = getSupabaseServer(); + const { data: existing, error: fetchError } = await supabase + .from('customer_claims') + .select('*') + .eq('id', claimId) + .eq('profile_id', companyId) + .single(); + if (fetchError || !existing) { + return NextResponse.json({ error: 'Claim not found' }, { status: 404 }); + } + + const fields = [ + 'status', + 'priority', + 'title', + 'description', + 'amount_claimed', + 'amount_approved', + 'resolution_notes', + 'owner_name', + 'claim_type', + ] as const; + const updates: Record = { updated_at: new Date().toISOString() }; + for (const f of fields) { + if (body[f] !== undefined) updates[f] = body[f]; + } + if (['resolved', 'closed', 'approved', 'rejected'].includes(String(body.status))) { + updates.resolved_at = new Date().toISOString(); + } + const { data, error } = await supabase + .from('customer_claims') + .update(updates) + .eq('id', claimId) + .eq('profile_id', companyId) + .select('*') + .single(); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ success: true, claim: data }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : 'Error' }, { status: 500 }); + } +} diff --git a/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-387bcf/vuln.ts b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-387bcf/vuln.ts new file mode 100644 index 0000000..a664037 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-387bcf/vuln.ts @@ -0,0 +1,47 @@ +// Provenance: bigfivefoods/supplieradvisor-mvp (vuln). +// repo: bigfivefoods/supplieradvisor-mvp +// commit: a7a9bc260d649ea2e582c07435bfd7d93100647b +// parent: a7a9bc260d649ea2e582c07435bfd7d93100647b +// commit_url: https://github.com/bigfivefoods/supplieradvisor-mvp/commit/387bcf40e984a2ee1832ab7beabf0284c5b627d8 +// cve: +// license: MIT +// function: PATCH +// relpath: app/api/customers/claims/route.ts +// provenance: agent +// mechanism: identity_from_request_body + +export async function PATCH(request: NextRequest) { + try { + const body = await request.json(); + if (!body.id) return NextResponse.json({ error: 'id required' }, { status: 400 }); + const fields = [ + 'status', + 'priority', + 'title', + 'description', + 'amount_claimed', + 'amount_approved', + 'resolution_notes', + 'owner_name', + 'claim_type', + ] as const; + const updates: Record = { updated_at: new Date().toISOString() }; + for (const f of fields) { + if (body[f] !== undefined) updates[f] = body[f]; + } + if (['resolved', 'closed', 'approved', 'rejected'].includes(String(body.status))) { + updates.resolved_at = new Date().toISOString(); + } + const supabase = getSupabaseServer(); + const { data, error } = await supabase + .from('customer_claims') + .update(updates) + .eq('id', Number(body.id)) + .select('*') + .single(); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ success: true, claim: data }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : 'Error' }, { status: 500 }); + } +} diff --git a/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-a69e8f/fixed.ts b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-a69e8f/fixed.ts new file mode 100644 index 0000000..19aa293 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-a69e8f/fixed.ts @@ -0,0 +1,157 @@ +// Provenance: bigfivefoods/supplieradvisor-mvp (fixed). +// repo: bigfivefoods/supplieradvisor-mvp +// commit: a69e8f27c0f4d402573d16f0612b51155bbc46e1 +// parent: 8558538471e4fb10856f8d9a17d2be5ab5c4fb9c +// commit_url: https://github.com/bigfivefoods/supplieradvisor-mvp/commit/a69e8f27c0f4d402573d16f0612b51155bbc46e1 +// cve: +// license: MIT +// function: GET +// relpath: app/api/customers/opportunities/route.ts +// provenance: agent +// mechanism: identity_from_request_body + +export async function GET(request: NextRequest) { + try { + const companyId = Number(request.nextUrl.searchParams.get('companyId')); + const stage = request.nextUrl.searchParams.get('stage'); + const q = (request.nextUrl.searchParams.get('q') || '').trim().toLowerCase(); + if (!Number.isFinite(companyId) || companyId <= 0) { + return NextResponse.json({ error: 'companyId required' }, { status: 400 }); + } + const _gate = await requireCompanyAccess(request, companyId, { legacyPrivyUserId: legacyPrivyFrom(request) }); + if (!_gate.ok) return _gate.response; + const supabase = getSupabaseServer(); + const { loadHoldingSubtree, annotateGroupOpportunity } = await import( + '@/lib/business/holding-pipeline' + ); + const tree = await loadHoldingSubtree(companyId); + let query = supabase + .from('opportunities') + .select('*') + .in('profile_id', tree.ids) + .order('updated_at', { ascending: false }) + .limit(2000); + if (stage && stage !== 'all') { + query = query.or(`stage.eq.${stage},status.eq.${stage}`); + } + + const { data, error } = await query; + if (error) { + return NextResponse.json({ + success: true, + opportunities: [], + warning: error.message, + hint: 'Run 20260709_crm_leads_opportunities.sql', + }); + } + + let opportunities: OpportunityListItem[] = (data || []).map((raw) => { + const o = raw as Record; + const st = normalizeStage( + o.stage as string | null, + o.status as string | null + ); + const amount = Number(o.amount ?? o.opportunity_size ?? 0); + const probability = + o.probability != null ? Number(o.probability) : stageProbability(st); + const openDate = + toDateOnly(o.open_date as string | null) || + toDateOnly(o.created_at as string | null) || + null; + const expectedClose = + toDateOnly(o.expected_close_date as string | null) || + toDateOnly(o.estimated_date as string | null) || + null; + const actualClose = toDateOnly(o.actual_close_date as string | null); + const annotated = annotateGroupOpportunity( + { ...o, stage: st, amount, probability }, + companyId, + tree.names + ); + return { + ...o, + ...annotated, + name: o.name ?? null, + contact_name: o.contact_name ?? null, + company_name: o.company_name ?? null, + contact_email: o.contact_email ?? null, + product_interest: o.product_interest ?? null, + stage: st, + amount, + probability, + open_date: openDate, + expected_close_date: expectedClose, + actual_close_date: actualClose, + contact_phone: o.contact_phone || o.contact_number || null, + location: o.location || o.opportunity_location || null, + description: o.description || o.notes || null, + weighted_amount: Math.round((amount * probability) / 100), + days_to_expected: + openDate && expectedClose + ? Math.round( + (new Date(expectedClose).getTime() - + new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : null, + days_to_close: + openDate && actualClose + ? Math.round( + (new Date(actualClose).getTime() - + new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : null, + days_open: + openDate && !actualClose + ? Math.round( + (Date.now() - new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : openDate && actualClose + ? Math.round( + (new Date(actualClose).getTime() - + new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : null, + }; + }); + + if (q) { + opportunities = opportunities.filter((o) => { + const hay = [ + searchHay(o.name), + searchHay(o.contact_name), + searchHay(o.company_name), + searchHay(o.contact_email), + searchHay(o.contact_phone), + searchHay(o.stage), + searchHay(o.product_interest), + ] + .join(' ') + .toLowerCase(); + return hay.includes(q); + }); + } + + const { summarizeGroupPipeline } = await import( + '@/lib/business/group-pipeline-view' + ); + const group = summarizeGroupPipeline({ + viewerCompanyId: companyId, + names: tree.names, + companyIds: tree.ids, + isSubsidiary: tree.isSubsidiary, + opportunities, + }); + + return NextResponse.json({ + success: true, + opportunities, + group, + }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : 'Error' }, { status: 500 }); + } +} diff --git a/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-a69e8f/vuln.ts b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-a69e8f/vuln.ts new file mode 100644 index 0000000..e28142d --- /dev/null +++ b/benchmarks/pairs/agent-vfc/supplieradvisor-mvp-route-a69e8f/vuln.ts @@ -0,0 +1,155 @@ +// Provenance: bigfivefoods/supplieradvisor-mvp (vuln). +// repo: bigfivefoods/supplieradvisor-mvp +// commit: 8558538471e4fb10856f8d9a17d2be5ab5c4fb9c +// parent: 8558538471e4fb10856f8d9a17d2be5ab5c4fb9c +// commit_url: https://github.com/bigfivefoods/supplieradvisor-mvp/commit/a69e8f27c0f4d402573d16f0612b51155bbc46e1 +// cve: +// license: MIT +// function: GET +// relpath: app/api/customers/opportunities/route.ts +// provenance: agent +// mechanism: identity_from_request_body + +export async function GET(request: NextRequest) { + try { + const companyId = Number(request.nextUrl.searchParams.get('companyId')); + const stage = request.nextUrl.searchParams.get('stage'); + const q = (request.nextUrl.searchParams.get('q') || '').trim().toLowerCase(); + if (!Number.isFinite(companyId)) { + return NextResponse.json({ error: 'companyId required' }, { status: 400 }); + } + const supabase = getSupabaseServer(); + const { loadHoldingSubtree, annotateGroupOpportunity } = await import( + '@/lib/business/holding-pipeline' + ); + const tree = await loadHoldingSubtree(companyId); + let query = supabase + .from('opportunities') + .select('*') + .in('profile_id', tree.ids) + .order('updated_at', { ascending: false }) + .limit(2000); + if (stage && stage !== 'all') { + query = query.or(`stage.eq.${stage},status.eq.${stage}`); + } + + const { data, error } = await query; + if (error) { + return NextResponse.json({ + success: true, + opportunities: [], + warning: error.message, + hint: 'Run 20260709_crm_leads_opportunities.sql', + }); + } + + let opportunities: OpportunityListItem[] = (data || []).map((raw) => { + const o = raw as Record; + const st = normalizeStage( + o.stage as string | null, + o.status as string | null + ); + const amount = Number(o.amount ?? o.opportunity_size ?? 0); + const probability = + o.probability != null ? Number(o.probability) : stageProbability(st); + const openDate = + toDateOnly(o.open_date as string | null) || + toDateOnly(o.created_at as string | null) || + null; + const expectedClose = + toDateOnly(o.expected_close_date as string | null) || + toDateOnly(o.estimated_date as string | null) || + null; + const actualClose = toDateOnly(o.actual_close_date as string | null); + const annotated = annotateGroupOpportunity( + { ...o, stage: st, amount, probability }, + companyId, + tree.names + ); + return { + ...o, + ...annotated, + name: o.name ?? null, + contact_name: o.contact_name ?? null, + company_name: o.company_name ?? null, + contact_email: o.contact_email ?? null, + product_interest: o.product_interest ?? null, + stage: st, + amount, + probability, + open_date: openDate, + expected_close_date: expectedClose, + actual_close_date: actualClose, + contact_phone: o.contact_phone || o.contact_number || null, + location: o.location || o.opportunity_location || null, + description: o.description || o.notes || null, + weighted_amount: Math.round((amount * probability) / 100), + days_to_expected: + openDate && expectedClose + ? Math.round( + (new Date(expectedClose).getTime() - + new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : null, + days_to_close: + openDate && actualClose + ? Math.round( + (new Date(actualClose).getTime() - + new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : null, + days_open: + openDate && !actualClose + ? Math.round( + (Date.now() - new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : openDate && actualClose + ? Math.round( + (new Date(actualClose).getTime() - + new Date(openDate).getTime()) / + (24 * 60 * 60 * 1000) + ) + : null, + }; + }); + + if (q) { + opportunities = opportunities.filter((o) => { + const hay = [ + searchHay(o.name), + searchHay(o.contact_name), + searchHay(o.company_name), + searchHay(o.contact_email), + searchHay(o.contact_phone), + searchHay(o.stage), + searchHay(o.product_interest), + ] + .join(' ') + .toLowerCase(); + return hay.includes(q); + }); + } + + const { summarizeGroupPipeline } = await import( + '@/lib/business/group-pipeline-view' + ); + const group = summarizeGroupPipeline({ + viewerCompanyId: companyId, + names: tree.names, + companyIds: tree.ids, + isSubsidiary: tree.isSubsidiary, + opportunities, + }); + + return NextResponse.json({ + success: true, + opportunities, + group, + }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : 'Error' }, { status: 500 }); + } +} diff --git a/benchmarks/pairs/agent-vfc/tools-cli-tools-a96bc0/fixed.py b/benchmarks/pairs/agent-vfc/tools-cli-tools-a96bc0/fixed.py new file mode 100644 index 0000000..435495b --- /dev/null +++ b/benchmarks/pairs/agent-vfc/tools-cli-tools-a96bc0/fixed.py @@ -0,0 +1,61 @@ +# Provenance: D-sorganization/Tools (fixed). +# repo: D-sorganization/Tools +# commit: a96bc0502a3dcdac303e4f0152508886e5c35c0a +# parent: e735d28b82dd1fbc42c0ae01157b38d1b4c7c864 +# commit_url: https://github.com/D-sorganization/Tools/commit/a96bc0502a3dcdac303e4f0152508886e5c35c0a +# cve: +# license: MIT +# function: _is_command_allowed +# relpath: src/shared/python/ai/tools/cli_tools.py +# provenance: agent +# mechanism: source_reaches_sink + + def _is_command_allowed(self, command: str) -> bool: + """Check if a command is allowed. + + Args: + command: Command to check. + + Returns: + True if allowed, False otherwise. + """ + # Dangerous commands are never allowed + dangerous = ["rm", "sudo", "chmod", "chown", "curl", "wget", "ssh"] + + # Prevent shell injection by blocking command separators/operators + shell_operators = ["&&", "||", ";", "|", ">", "<", "$", "`", "\n", "&"] + if any(op in command for op in shell_operators): + return False + + try: + tokens = shlex.split(command) + if not tokens: + return False + + # Verify the first token is in the allowlist + base_cmd = tokens[0] + if base_cmd not in self._allowed_commands: + return False + + # Verify no token is a dangerous command + for token in tokens: + if token in dangerous: + return False + + try: + # Check for absolute/relative paths (e.g., /bin/rm, ./rm) + if Path(token).name in dangerous: + return False + + # Check for assignments passing executables (e.g., --exec=/bin/rm) + if "=" in token: + val = token.split("=", 1)[1] + if val in dangerous or Path(val).name in dangerous: + return False + except Exception: + pass + + return True + except ValueError: + # e.g., missing closing quote + return False diff --git a/benchmarks/pairs/agent-vfc/tools-cli-tools-a96bc0/vuln.py b/benchmarks/pairs/agent-vfc/tools-cli-tools-a96bc0/vuln.py new file mode 100644 index 0000000..64e726e --- /dev/null +++ b/benchmarks/pairs/agent-vfc/tools-cli-tools-a96bc0/vuln.py @@ -0,0 +1,48 @@ +# Provenance: D-sorganization/Tools (vuln). +# repo: D-sorganization/Tools +# commit: e735d28b82dd1fbc42c0ae01157b38d1b4c7c864 +# parent: e735d28b82dd1fbc42c0ae01157b38d1b4c7c864 +# commit_url: https://github.com/D-sorganization/Tools/commit/a96bc0502a3dcdac303e4f0152508886e5c35c0a +# cve: +# license: MIT +# function: _is_command_allowed +# relpath: src/shared/python/ai/tools/cli_tools.py +# provenance: agent +# mechanism: source_reaches_sink + + def _is_command_allowed(self, command: str) -> bool: + """Check if a command is allowed. + + Args: + command: Command to check. + + Returns: + True if allowed, False otherwise. + """ + # Dangerous commands are never allowed + dangerous = ["rm", "sudo", "chmod", "chown", "curl", "wget", "ssh"] + + # Prevent shell injection by blocking command separators/operators + shell_operators = ["&&", "||", ";", "|", ">", "<", "$", "`", "\n", "&"] + if any(op in command for op in shell_operators): + return False + + try: + tokens = shlex.split(command) + if not tokens: + return False + + # Verify the first token is in the allowlist + base_cmd = tokens[0] + if base_cmd not in self._allowed_commands: + return False + + # Verify no token is a dangerous command + for token in tokens: + if token in dangerous: + return False + + return True + except ValueError: + # e.g., missing closing quote + return False diff --git a/benchmarks/pairs/agent-vfc/trackly-cli-path-diagnostics-1c1aed/fixed.js b/benchmarks/pairs/agent-vfc/trackly-cli-path-diagnostics-1c1aed/fixed.js new file mode 100644 index 0000000..6b09e95 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/trackly-cli-path-diagnostics-1c1aed/fixed.js @@ -0,0 +1,48 @@ +// Provenance: trackly-app/trackly-cli (fixed). +// repo: trackly-app/trackly-cli +// commit: 1c1aed89b07edb04051830c8f086e02b65e961c6 +// parent: 8d240ad8d7fc114c8f941ad956a2fdf32391ff60 +// commit_url: https://github.com/trackly-app/trackly-cli/commit/1c1aed89b07edb04051830c8f086e02b65e961c6 +// cve: +// license: MIT +// function: inspectExistingAncestor +// relpath: lib/path-diagnostics.js +// provenance: agent +// mechanism: path_join_user_input + +async function inspectExistingAncestor(exactPath, stat = fs.stat) { + let candidate = exactPath; + while (true) { + try { + const stats = await stat(candidate); + return { + ancestor: candidate, + exactPathExists: candidate === exactPath, + exactPathType: candidate === exactPath ? (stats.isDirectory() ? 'directory' : 'file') : null, + writableDirectory: stats.isDirectory() ? candidate : path.dirname(candidate), + inspectionErrorCode: null, + }; + } catch (error) { + if (error.code !== 'ENOENT') { + return { + ancestor: candidate, + exactPathExists: null, + exactPathType: null, + writableDirectory: null, + inspectionErrorCode: error.code || 'path_inspection_failed', + }; + } + const parent = path.dirname(candidate); + if (parent === candidate) { + return { + ancestor: parent, + exactPathExists: false, + exactPathType: null, + writableDirectory: parent, + inspectionErrorCode: null, + }; + } + candidate = parent; + } + } +} diff --git a/benchmarks/pairs/agent-vfc/trackly-cli-path-diagnostics-1c1aed/vuln.js b/benchmarks/pairs/agent-vfc/trackly-cli-path-diagnostics-1c1aed/vuln.js new file mode 100644 index 0000000..37ed0ce --- /dev/null +++ b/benchmarks/pairs/agent-vfc/trackly-cli-path-diagnostics-1c1aed/vuln.js @@ -0,0 +1,33 @@ +// Provenance: trackly-app/trackly-cli (vuln). +// repo: trackly-app/trackly-cli +// commit: 8d240ad8d7fc114c8f941ad956a2fdf32391ff60 +// parent: 8d240ad8d7fc114c8f941ad956a2fdf32391ff60 +// commit_url: https://github.com/trackly-app/trackly-cli/commit/1c1aed89b07edb04051830c8f086e02b65e961c6 +// cve: +// license: MIT +// function: inspectExistingAncestor +// relpath: lib/path-diagnostics.js +// provenance: agent +// mechanism: path_join_user_input + +async function inspectExistingAncestor(exactPath) { + let candidate = exactPath; + while (true) { + try { + const stats = await fs.stat(candidate); + return { + ancestor: candidate, + exactPathExists: candidate === exactPath, + exactPathType: candidate === exactPath ? (stats.isDirectory() ? 'directory' : 'file') : null, + writableDirectory: stats.isDirectory() ? candidate : path.dirname(candidate), + }; + } catch (error) { + if (error.code !== 'ENOENT') throw error; + const parent = path.dirname(candidate); + if (parent === candidate) { + return { ancestor: parent, exactPathExists: false, exactPathType: null, writableDirectory: parent }; + } + candidate = parent; + } + } +} diff --git a/benchmarks/pairs/agent-vfc/training/manifest.jsonl b/benchmarks/pairs/agent-vfc/training/manifest.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/benchmarks/pairs/agent-vfc/upstreamdrift-local-server-c3a078/fixed.py b/benchmarks/pairs/agent-vfc/upstreamdrift-local-server-c3a078/fixed.py new file mode 100644 index 0000000..5c7e56a --- /dev/null +++ b/benchmarks/pairs/agent-vfc/upstreamdrift-local-server-c3a078/fixed.py @@ -0,0 +1,33 @@ +# Provenance: D-sorganization/UpstreamDrift (fixed). +# repo: D-sorganization/UpstreamDrift +# commit: c3a0787080489b30e2ed0abfac03b51e45da184b +# parent: e28d1da5e103d93951ef11b78a6e9b89ac1fa2bc +# commit_url: https://github.com/D-sorganization/UpstreamDrift/commit/c3a0787080489b30e2ed0abfac03b51e45da184b +# cve: +# license: MIT +# function: _find_logo_file +# relpath: src/api/local_server.py +# provenance: agent +# mechanism: path_join_user_input + +def _find_logo_file(logo_name: str) -> Path | None: + """Search for a logo file in known asset directories. + + The caller-supplied ``logo_name`` is joined onto each candidate root via + :func:`_safe_join` so that ``..`` traversal, absolute paths, and symlink + escape are rejected before touching the filesystem. + + Args: + logo_name: Filename of the logo to find. + + Returns: + Path to the logo file, or None if not found or rejected as unsafe. + """ + for root in ( + Path(__file__).parent.parent.parent / "assets" / "logos", + Path(__file__).parent.parent / "launchers" / "assets", + ): + resolved = _safe_join(root, logo_name) + if resolved is not None and resolved.exists() and resolved.is_file(): + return resolved + return None diff --git a/benchmarks/pairs/agent-vfc/upstreamdrift-local-server-c3a078/vuln.py b/benchmarks/pairs/agent-vfc/upstreamdrift-local-server-c3a078/vuln.py new file mode 100644 index 0000000..4767a57 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/upstreamdrift-local-server-c3a078/vuln.py @@ -0,0 +1,30 @@ +# Provenance: D-sorganization/UpstreamDrift (vuln). +# repo: D-sorganization/UpstreamDrift +# commit: e28d1da5e103d93951ef11b78a6e9b89ac1fa2bc +# parent: e28d1da5e103d93951ef11b78a6e9b89ac1fa2bc +# commit_url: https://github.com/D-sorganization/UpstreamDrift/commit/c3a0787080489b30e2ed0abfac03b51e45da184b +# cve: +# license: MIT +# function: _find_logo_file +# relpath: src/api/local_server.py +# provenance: agent +# mechanism: path_join_user_input + +def _find_logo_file(logo_name: str) -> Path | None: + """Search for a logo file in known asset directories. + + Args: + logo_name: Filename of the logo to find. + + Returns: + Path to the logo file, or None if not found. + """ + logos_dir = Path(__file__).parent.parent.parent / "assets" / "logos" + logo_path = logos_dir / logo_name + if logo_path.exists() and logo_path.is_file(): + return logo_path + launcher_logos = Path(__file__).parent.parent / "launchers" / "assets" + alt_path = launcher_logos / logo_name + if alt_path.exists() and alt_path.is_file(): + return alt_path + return None diff --git a/benchmarks/pairs/agent-vfc/waggledance-swarm-ssrf-host-guard-9a4548/fixed.py b/benchmarks/pairs/agent-vfc/waggledance-swarm-ssrf-host-guard-9a4548/fixed.py new file mode 100644 index 0000000..ead06d2 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/waggledance-swarm-ssrf-host-guard-9a4548/fixed.py @@ -0,0 +1,28 @@ +# Provenance: Ahkeratmehilaiset/waggledance-swarm (fixed). +# repo: Ahkeratmehilaiset/waggledance-swarm +# commit: 9a454843aa35332ff2eb3106023eee0c4b5e444b +# parent: 934b939f1cca5259eaf807db3319eb97b90d8d80 +# commit_url: https://github.com/Ahkeratmehilaiset/waggledance-swarm/commit/9a454843aa35332ff2eb3106023eee0c4b5e444b +# cve: +# license: Apache-2.0 +# function: normalize_allowed_hosts +# relpath: waggledance/core/v3_13_0/ssrf_host_guard.py +# provenance: agent +# mechanism: source_reaches_sink + +def normalize_allowed_hosts(raw_hosts: Iterable[str]) -> frozenset[str]: + """Validate and canonicalize an exact-host allowlist.""" + if ( + not isinstance(raw_hosts, Iterable) + or isinstance(raw_hosts, (str, bytes, Mapping)) + ): + raise ValueError("ALLOWLIST_HOSTS_REFUSED") + normalized: set[str] = set() + for index, host in enumerate(raw_hosts): + if not isinstance(host, str) or not host.strip(): + raise ValueError(f"ALLOWLIST_HOSTS_REFUSED_{index}") + clean = normalize_host(host) + if "://" in clean or "/" in clean or "?" in clean or "@" in clean: + raise ValueError(f"ALLOWLIST_HOSTS_REFUSED_{index}") + normalized.add(clean) + return frozenset(normalized) diff --git a/benchmarks/pairs/agent-vfc/waggledance-swarm-ssrf-host-guard-9a4548/vuln.py b/benchmarks/pairs/agent-vfc/waggledance-swarm-ssrf-host-guard-9a4548/vuln.py new file mode 100644 index 0000000..657b944 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/waggledance-swarm-ssrf-host-guard-9a4548/vuln.py @@ -0,0 +1,25 @@ +# Provenance: Ahkeratmehilaiset/waggledance-swarm (vuln). +# repo: Ahkeratmehilaiset/waggledance-swarm +# commit: 934b939f1cca5259eaf807db3319eb97b90d8d80 +# parent: 934b939f1cca5259eaf807db3319eb97b90d8d80 +# commit_url: https://github.com/Ahkeratmehilaiset/waggledance-swarm/commit/9a454843aa35332ff2eb3106023eee0c4b5e444b +# cve: +# license: Apache-2.0 +# function: normalize_allowed_hosts +# relpath: waggledance/core/v3_13_0/ssrf_host_guard.py +# provenance: agent +# mechanism: source_reaches_sink + +def normalize_allowed_hosts(raw_hosts: Sequence[str]) -> frozenset[str]: + """Validate and canonicalize an exact-host allowlist.""" + if not isinstance(raw_hosts, Sequence) or isinstance(raw_hosts, (str, bytes)): + raise ValueError("ALLOWLIST_HOSTS_REFUSED") + normalized: set[str] = set() + for index, host in enumerate(raw_hosts): + if not isinstance(host, str) or not host.strip(): + raise ValueError(f"ALLOWLIST_HOSTS_REFUSED_{index}") + clean = normalize_host(host) + if "://" in clean or "/" in clean or "?" in clean or "@" in clean: + raise ValueError(f"ALLOWLIST_HOSTS_REFUSED_{index}") + normalized.add(clean) + return frozenset(normalized) diff --git a/benchmarks/pairs/agent-vfc/workers-research-utils-209444/fixed.ts b/benchmarks/pairs/agent-vfc/workers-research-utils-209444/fixed.ts new file mode 100644 index 0000000..abe9a13 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/workers-research-utils-209444/fixed.ts @@ -0,0 +1,162 @@ +// Provenance: G4brym/workers-research (fixed). +// repo: G4brym/workers-research +// commit: 209444eb61cc564c2d6b43267256f85fe3e5e30e +// parent: eab923ad3099c01b002fbd6be7cf994614bbdee4 +// commit_url: https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e +// cve: +// license: MIT +// function: formatDuration +// relpath: src/utils.ts +// provenance: agent +// mechanism: source_reaches_sink + +import { + createGoogleGenerativeAI, + type GoogleGenerativeAIProvider, + type GoogleGenerativeAIProviderSettings, +} from "@ai-sdk/google"; +import type { Env } from "./bindings"; + +function getGoogleProvider(env: Env): GoogleGenerativeAIProvider { + const args: GoogleGenerativeAIProviderSettings = { + apiKey: env.GOOGLE_API_KEY, + }; + + if (env.AI_GATEWAY_ACCOUNT_ID && env.AI_GATEWAY_NAME) { + args.baseURL = `https://gateway.ai.cloudflare.com/v1/${env.AI_GATEWAY_ACCOUNT_ID}/${env.AI_GATEWAY_NAME}/google-ai-studio/v1beta`; + + if (env.AI_GATEWAY_API_KEY) { + args.headers = { + "cf-aig-authorization": `Bearer ${env.AI_GATEWAY_API_KEY}`, + }; + } + } + + return createGoogleGenerativeAI(args); +} + +export function getModel(env: Env) { + const google = getGoogleProvider(env); + + return google("gemini-2.0-flash-exp"); +} + +export function getFallbackModel(env: Env) { + const google = getGoogleProvider(env); + return google("gemini-2.0-flash"); +} + +export function getModelThinking(env: Env) { + const google = getGoogleProvider(env); + + return google("gemini-2.0-flash-exp"); +} + +export function timeAgo(date: Date): string { + const now = new Date(); + const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + const intervals: [number, string][] = [ + [60, "minute"], + [60, "hour"], + [24, "day"], + [7, "week"], + [4.35, "month"], + [12, "year"], + ]; + + let count = seconds; + let unit = "second"; + + for (const [interval, name] of intervals) { + if (count < interval) break; + count /= interval; + unit = name; + } + + count = Math.floor(count); + return `${count} ${unit}${count !== 1 ? "s" : ""} ago`; +} + +export async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Normalize a domain input for excluded domain matching. + * Handles common user input formats: + * - Full URLs: "https://reddit.com/foo" → "reddit.com" + * - www prefix: "www.reddit.com" → "reddit.com" + * - Mixed: "https://www.reddit.com" → "reddit.com" + * - Plain domains are returned as-is (lowercased, trimmed) + */ +export function normalizeDomain(input: string): string { + let domain = input.trim().toLowerCase(); + if (domain.length === 0) return ""; + + // If it looks like a URL (has ://), extract the hostname + if (domain.includes("://")) { + try { + domain = new URL(domain).hostname; + } catch { + // If URL parsing fails, strip the protocol manually + domain = domain.replace(/^[a-z]+:\/\//, "").split("/")[0]; + } + } else { + // Strip any trailing path from plain domain input + domain = domain.split("/")[0]; + } + + // Strip www. prefix for consistent matching + domain = domain.replace(/^www\./, ""); + + return domain; +} + +/** + * Build parameterized search filter conditions for research list queries. + * Uses parameterized queries to prevent SQL injection. + */ +export function buildSearchFilters( + q?: string, + status?: string, +): { conditions: string[]; params: (string | number)[] } { + const conditions: string[] = []; + const params: (string | number)[] = []; + + if (q?.trim()) { + const searchTerm = `%${q.trim()}%`; + conditions.push("(title LIKE ? OR query LIKE ?)"); + params.push(searchTerm, searchTerm); + } + + if (status && ["1", "2", "3"].includes(status)) { + conditions.push("status = ?"); + params.push(Number.parseInt(status, 10)); + } + + return { conditions, params }; +} + +export function formatDuration(ms: number): string { + // Handle negative or zero values + if (ms <= 0) { + return "0.0 seconds"; + } + + const seconds = ms / 1000; + const minutes = seconds / 60; + const hours = minutes / 60; + const days = hours / 24; + + // Determine the appropriate unit and format with one decimal place + if (days >= 1) { + return `${days.toFixed(1)} day${days !== 1 ? "s" : ""}`; + } else if (hours >= 1) { + return `${hours.toFixed(1)} hour${hours !== 1 ? "s" : ""}`; + } else if (minutes >= 1) { + return `${minutes.toFixed(1)} minute${minutes !== 1 ? "s" : ""}`; + } else { + return `${seconds.toFixed(1)} second${seconds !== 1 ? "s" : ""}`; + } +} diff --git a/benchmarks/pairs/agent-vfc/workers-research-utils-209444/vuln.ts b/benchmarks/pairs/agent-vfc/workers-research-utils-209444/vuln.ts new file mode 100644 index 0000000..9b70bb8 --- /dev/null +++ b/benchmarks/pairs/agent-vfc/workers-research-utils-209444/vuln.ts @@ -0,0 +1,137 @@ +// Provenance: G4brym/workers-research (vuln). +// repo: G4brym/workers-research +// commit: eab923ad3099c01b002fbd6be7cf994614bbdee4 +// parent: eab923ad3099c01b002fbd6be7cf994614bbdee4 +// commit_url: https://github.com/G4brym/workers-research/commit/209444eb61cc564c2d6b43267256f85fe3e5e30e +// cve: +// license: MIT +// function: formatDuration +// relpath: src/utils.ts +// provenance: agent +// mechanism: source_reaches_sink + +import { + createGoogleGenerativeAI, + type GoogleGenerativeAIProvider, + type GoogleGenerativeAIProviderSettings, +} from "@ai-sdk/google"; +import type { Env } from "./bindings"; + +function getGoogleProvider(env: Env): GoogleGenerativeAIProvider { + const args: GoogleGenerativeAIProviderSettings = { + apiKey: env.GOOGLE_API_KEY, + }; + + if (env.AI_GATEWAY_ACCOUNT_ID && env.AI_GATEWAY_NAME) { + args.baseURL = `https://gateway.ai.cloudflare.com/v1/${env.AI_GATEWAY_ACCOUNT_ID}/${env.AI_GATEWAY_NAME}/google-ai-studio/v1beta`; + + if (env.AI_GATEWAY_API_KEY) { + args.headers = { + "cf-aig-authorization": `Bearer ${env.AI_GATEWAY_API_KEY}`, + }; + } + } + + return createGoogleGenerativeAI(args); +} + +export function getModel(env: Env) { + const google = getGoogleProvider(env); + + return google("gemini-2.0-flash-exp"); +} + +export function getFallbackModel(env: Env) { + const google = getGoogleProvider(env); + return google("gemini-2.0-flash"); +} + +export function getModelThinking(env: Env) { + const google = getGoogleProvider(env); + + return google("gemini-2.0-flash-exp"); +} + +export function timeAgo(date: Date): string { + const now = new Date(); + const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + const intervals: [number, string][] = [ + [60, "minute"], + [60, "hour"], + [24, "day"], + [7, "week"], + [4.35, "month"], + [12, "year"], + ]; + + let count = seconds; + let unit = "second"; + + for (const [interval, name] of intervals) { + if (count < interval) break; + count /= interval; + unit = name; + } + + count = Math.floor(count); + return `${count} ${unit}${count !== 1 ? "s" : ""} ago`; +} + +export async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Normalize a domain input for excluded domain matching. + * Handles common user input formats: + * - Full URLs: "https://reddit.com/foo" → "reddit.com" + * - www prefix: "www.reddit.com" → "reddit.com" + * - Mixed: "https://www.reddit.com" → "reddit.com" + * - Plain domains are returned as-is (lowercased, trimmed) + */ +export function normalizeDomain(input: string): string { + let domain = input.trim().toLowerCase(); + if (domain.length === 0) return ""; + + // If it looks like a URL (has ://), extract the hostname + if (domain.includes("://")) { + try { + domain = new URL(domain).hostname; + } catch { + // If URL parsing fails, strip the protocol manually + domain = domain.replace(/^[a-z]+:\/\//, "").split("/")[0]; + } + } else { + // Strip any trailing path from plain domain input + domain = domain.split("/")[0]; + } + + // Strip www. prefix for consistent matching + domain = domain.replace(/^www\./, ""); + + return domain; +} + +export function formatDuration(ms: number): string { + // Handle negative or zero values + if (ms <= 0) { + return "0.0 seconds"; + } + + const seconds = ms / 1000; + const minutes = seconds / 60; + const hours = minutes / 60; + const days = hours / 24; + + // Determine the appropriate unit and format with one decimal place + if (days >= 1) { + return `${days.toFixed(1)} day${days !== 1 ? "s" : ""}`; + } else if (hours >= 1) { + return `${hours.toFixed(1)} hour${hours !== 1 ? "s" : ""}`; + } else if (minutes >= 1) { + return `${minutes.toFixed(1)} minute${minutes !== 1 ? "s" : ""}`; + } else { + return `${seconds.toFixed(1)} second${seconds !== 1 ? "s" : ""}`; + } +} diff --git a/benchmarks/pairs/agent-vfc/zoonk-origin-bfbf67/fixed.ts b/benchmarks/pairs/agent-vfc/zoonk-origin-bfbf67/fixed.ts new file mode 100644 index 0000000..b4b5f8e --- /dev/null +++ b/benchmarks/pairs/agent-vfc/zoonk-origin-bfbf67/fixed.ts @@ -0,0 +1,19 @@ +// Provenance: zoonk/zoonk (fixed). +// repo: zoonk/zoonk +// commit: bfbf670ccb41d63c407d32c51bd26e626a174f31 +// parent: 2604269350bff51f58572c133b45010cb0976a5a +// commit_url: https://github.com/zoonk/zoonk/commit/bfbf670ccb41d63c407d32c51bd26e626a174f31 +// cve: +// license: MIT +// function: getAllowedHosts +// relpath: packages/utils/src/origin.ts +// provenance: agent +// mechanism: permissive_default + +export function getAllowedHosts(): string[] { + return [ + ...ZOONK_DOMAINS.flatMap((domain) => [domain, `*.${domain}`]), + ...(isLocalhostSupported() ? ["localhost:*"] : []), + ...(canAcceptVercelPreviewOrigins() ? [ZOONK_VERCEL_PREVIEW_HOST] : []), + ]; +} diff --git a/benchmarks/pairs/agent-vfc/zoonk-origin-bfbf67/vuln.ts b/benchmarks/pairs/agent-vfc/zoonk-origin-bfbf67/vuln.ts new file mode 100644 index 0000000..b213c4e --- /dev/null +++ b/benchmarks/pairs/agent-vfc/zoonk-origin-bfbf67/vuln.ts @@ -0,0 +1,19 @@ +// Provenance: zoonk/zoonk (vuln). +// repo: zoonk/zoonk +// commit: 2604269350bff51f58572c133b45010cb0976a5a +// parent: 2604269350bff51f58572c133b45010cb0976a5a +// commit_url: https://github.com/zoonk/zoonk/commit/bfbf670ccb41d63c407d32c51bd26e626a174f31 +// cve: +// license: MIT +// function: getAllowedHosts +// relpath: packages/utils/src/origin.ts +// provenance: agent +// mechanism: permissive_default + +export function getAllowedHosts(): string[] { + return [ + ...ZOONK_DOMAINS.flatMap((domain) => [domain, `*.${domain}`]), + ...(isLocalhostSupported() ? ["localhost:*"] : []), + ...(getEnvironment() === "production" ? [] : ["*-zoonk.vercel.app"]), + ]; +} diff --git a/benchmarks/pairs/datasets.toml b/benchmarks/pairs/datasets.toml index 7bbb9e8..f4025d8 100644 --- a/benchmarks/pairs/datasets.toml +++ b/benchmarks/pairs/datasets.toml @@ -151,3 +151,63 @@ url = "https://github.com/curl/curl" level = "commit/function" languages = ["C"] notes = "Public git. Seed: CVE-2023-38545 do_SOCKS5. Isolated functions only; do not vendor the tree." + +# --- pair-corpus-honesty: web, agent-authored, and LLM-generated corpora --- + +[[dataset]] +name = "Real-Vuln-Benchmark" +url = "https://github.com/kolega-ai/Real-Vuln-Benchmark" +data = "benchmarks/pairs/vibe-py" +level = "repo/function/line" +languages = ["Python"] +notes = "Apache-2.0 (v2). 66 pinned Python web repos: 26 human-authored, 40 LLM-generated (Claude, GPT, Kimi) seeded with reviewed vulns. 1896 vulnerable findings + 280 false-positive traps with file, line range, function, primary/acceptable CWEs, severity, authorship. Educational/CTF style, not production. Source of the `vibe-py` slice; traps are the fixed-side twins." + +[[dataset]] +name = "SecBench.js" +url = "https://github.com/cristianstaicu/SecBench.js" +level = "package/sink-line" +languages = ["JavaScript"] +notes = "600 real npm vulnerabilities (prototype pollution, ReDoS, command injection, path traversal, code injection) with Jest exploit oracles, fixCommit, fixedVersion, and sinkLocation. No license file: never vendor its test files; harvest function pairs from the upstream package repositories named in fixCommit (`vfc-js` slice) and record each package's own license." + +[[dataset]] +name = "AIDev" +url = "https://huggingface.co/datasets/hao-li/AIDev" +level = "pull request" +languages = ["multi"] +notes = "932k agent-authored PRs (Claude Code, Codex, Copilot, Cursor, Devin, Jules) with patches and language; CC-BY-4.0 index, source repos keep their licenses. No security labels. Candidate source for `agent-vfc` together with GitHub commit search on agent co-author trailers." + +[[dataset]] +name = "BaxBench" +url = "https://github.com/logic-star-ai/baxbench" +data = "https://huggingface.co/datasets/LogicStar/BaxBench" +level = "scenario/exploit" +languages = ["Python", "JavaScript", "TypeScript", "Go", "Rust", "PHP"] +notes = "MIT. 28 backend scenarios x 14 frameworks (Express, Flask, FastAPI, Django, Nest, ...) with functional tests and executable exploits per CWE. Generated code is not shipped; generate with any model, the exploit harness labels each sample. Deferred slice (`baxgen`) until deep mode can run its exploits." + +[[dataset]] +name = "SecurityEval" +url = "https://github.com/s2e-lab/SecurityEval" +level = "prompt/sample" +languages = ["Python"] +notes = "121 prompts over 69 CWEs with Copilot and InCoder outputs. Vibe-textbook only; not web recall." + +[[dataset]] +name = "CodeSecEval" +url = "https://arxiv.org/abs/2407.02395" +level = "sample" +languages = ["Python"] +notes = "180 samples, 44 vulnerability types, insecure and secure executable code with tests. Pointer only." + +[[dataset]] +name = "CWEval" +url = "https://github.com/Co1lin/CWEval" +level = "task" +languages = ["C", "Python", "JavaScript"] +notes = "Functionality plus security evaluation of LLM-generated code on the same tasks. Pointer only." + +[[dataset]] +name = "CyberSecEval insecure code" +url = "https://github.com/meta-llama/PurpleLlama/tree/main/CybersecurityBenchmarks" +level = "test case" +languages = ["C", "C++", "C#", "Java", "JavaScript", "PHP", "Python", "Rust"] +notes = "MIT. Labels are DEFINED by its Insecure Code Detector regex/semgrep/weggli rules. Circular for a rule-proposing tool: coverage pointer only, never recall ground truth." diff --git a/benchmarks/pairs/mechanisms.toml b/benchmarks/pairs/mechanisms.toml new file mode 100644 index 0000000..3e3f967 --- /dev/null +++ b/benchmarks/pairs/mechanisms.toml @@ -0,0 +1,83 @@ +# Closed mechanism vocabulary for pair labels (pair-corpus-honesty, Req 2.1). +# Every [[pair.expected]] row names one of these ids. Bump `version` when the set changes. +version = "1" + +[[mechanism]] +id = "source_reaches_sink" +definition = "A fact-listed source flows by name into a fact-listed sink without a sanitizer." + +[[mechanism]] +id = "container_taint" +definition = "Taint travels through a mutable container or config object (list.append, dict[key], ConfigParser.set/get) before the sink." + +[[mechanism]] +id = "config_sink_weak_literal" +definition = "A configuration sink is dangerous because of a literal argument (hashlib.new('md5'), MessageDigest 'SHA-1', debug=True)." + +[[mechanism]] +id = "unchecked_length_copy" +definition = "A copy or read uses a length or index that is not bounded by a dominating check." + +[[mechanism]] +id = "unchecked_alloc_size" +definition = "An allocation size derives from untrusted or unbounded input." + +[[mechanism]] +id = "null_deref_unguarded" +definition = "A pointer that may be NULL is dereferenced without a dominating NULL test." + +[[mechanism]] +id = "signed_size_operand" +definition = "A signed or narrower type feeds a size or length operand and can wrap." + +[[mechanism]] +id = "fixed_buffer_capacity" +definition = "A fixed-size buffer receives data larger than its declared capacity." + +[[mechanism]] +id = "identity_from_request_body" +definition = "A user or owner identity is taken from the request body or query instead of the authenticated context." + +[[mechanism]] +id = "missing_auth_guard" +definition = "A route or handler performs a privileged action with no authentication or authorization guard on its path." + +[[mechanism]] +id = "secret_in_client" +definition = "A secret or privileged key is shipped to client-side code or committed configuration." + +[[mechanism]] +id = "permissive_default" +definition = "A security control is left at a permissive default (RLS off, CORS wildcard, debug mode, open bucket)." + +[[mechanism]] +id = "path_join_user_input" +definition = "A filesystem path is built from user input without normalization or containment." + +[[mechanism]] +id = "prototype_pollution" +definition = "Attacker-controlled keys reach a recursive merge or assignment that writes Object.prototype." + +[[mechanism]] +id = "redos" +definition = "A regular expression with catastrophic backtracking is applied to untrusted input." + +[[mechanism]] +id = "uaf" +definition = "Memory is used after it was released." + +[[mechanism]] +id = "type_confusion" +definition = "A value is interpreted as a type it is not." + +[[mechanism]] +id = "validation_strength" +definition = "A validator exists but accepts inputs it should reject; the fix strengthens the check." + +[[mechanism]] +id = "cross_artifact" +definition = "The bug is only decidable with an artifact outside the vendored files (a properties file, a schema, a build flag)." + +[[mechanism]] +id = "other" +definition = "Not yet classified. Present so bulk-mined pairs load; review should replace it." diff --git a/benchmarks/pairs/vfc-js/README.md b/benchmarks/pairs/vfc-js/README.md new file mode 100644 index 0000000..5edc5a3 --- /dev/null +++ b/benchmarks/pairs/vfc-js/README.md @@ -0,0 +1,14 @@ +# vfc-js slice + +Server-side JavaScript function pairs harvested from the **upstream fix commits** that +[SecBench.js](https://github.com/cristianstaicu/SecBench.js) records in its package metadata +(`fixCommit`, `sink`). SecBench.js itself has no license file, so none of its files are vendored; +each excerpt carries the upstream package's license. + +- Classes: command injection, path traversal, code injection first. Prototype pollution and ReDoS + rows may be added with their own mechanism and `known_limit` until the engine can talk about them. +- Mode: `hunk` (the function enclosing the first changed hunk on each side). +- Provenance: `human`. + +Rebuild: `python benchmarks/pairs/vfc-js/build_recipes.py --crawl /tmp/secbench.jsonl` then +`--from /tmp/secbench.jsonl`, `harvest.py --slice vfc-js --all --fetch`, `catalog_gen.py --slice vfc-js`. diff --git a/benchmarks/pairs/vfc-js/angular-http-server-1-0-0/fixed.js b/benchmarks/pairs/vfc-js/angular-http-server-1-0-0/fixed.js new file mode 100644 index 0000000..026e042 --- /dev/null +++ b/benchmarks/pairs/vfc-js/angular-http-server-1-0-0/fixed.js @@ -0,0 +1,55 @@ +// Provenance: simonh1000/angular-http-server (fixed). +// repo: simonh1000/angular-http-server +// commit: 8bafc9577161469f5dea01e0b98ea9c525d063e9 +// parent: d171a771a92e4685b24fb77cb894a7b7b8024c5c +// commit_url: https://github.com/simonh1000/angular-http-server/commit/8bafc9577161469f5dea01e0b98ea9c525d063e9 +// cve: +// license: Apache-2.0 +// function: requestListener +// relpath: angular-http-server.js +// provenance: human +// mechanism: path_join_user_input +// upstream_start: 45 + +function requestListener(req, res) { + // Add CORS header if option chosen + if (argv.cors) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Request-Method', '*'); + res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); + res.setHeader('Access-Control-Allow-Headers', 'authorization, content-type'); + // When the request is for CORS OPTIONS (rather than a page) return just the headers + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + } + + // Request is for a page instead + // Only interested in the part before any query params + var url = req.url.split('?')[0] + // Attaches path prefix with --path option + var possibleFilename = resolveUrl(url.slice(1)) || "dummy"; + + var safeFileName = path.normalize(possibleFilename).replace(/^(\.\.[\/\\])+/, ''); + var safeFullFilename = path.join(__dirname, safeFileName); + + fs.stat(safeFullFilename, function(err, stats) { + var fileBuffer; + if (!err && stats.isFile()) { + fileBuffer = fs.readFileSync(safeFullFilename); + let ct = mime.lookup(safeFullFilename); + log(`Sending ${safeFullFilename} with Content-Type ${ct}`); + res.writeHead(200, { 'Content-Type': ct }); + + } else { + log("Route %s, replacing with index.html", safeFullFilename); + fileBuffer = returnDistFile(); + res.writeHead(200, { 'Content-Type': 'text/html' }); + } + + res.write(fileBuffer); + res.end(); + }); +} diff --git a/benchmarks/pairs/vfc-js/angular-http-server-1-0-0/vuln.js b/benchmarks/pairs/vfc-js/angular-http-server-1-0-0/vuln.js new file mode 100644 index 0000000..e76f630 --- /dev/null +++ b/benchmarks/pairs/vfc-js/angular-http-server-1-0-0/vuln.js @@ -0,0 +1,52 @@ +// Provenance: simonh1000/angular-http-server (vuln). +// repo: simonh1000/angular-http-server +// commit: d171a771a92e4685b24fb77cb894a7b7b8024c5c +// parent: d171a771a92e4685b24fb77cb894a7b7b8024c5c +// commit_url: https://github.com/simonh1000/angular-http-server/commit/8bafc9577161469f5dea01e0b98ea9c525d063e9 +// cve: +// license: Apache-2.0 +// function: requestListener +// relpath: angular-http-server.js +// provenance: human +// mechanism: path_join_user_input +// upstream_start: 45 + +function requestListener(req, res) { + // Add CORS header if option chosen + if (argv.cors) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Request-Method', '*'); + res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); + res.setHeader('Access-Control-Allow-Headers', 'authorization, content-type'); + // When the request is for CORS OPTIONS (rather than a page) return just the headers + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + } + + // Request is for a page instead + // Only interested in the part before any query params + var url = req.url.split('?')[0] + // Attaches path prefix with --path option + var possibleFilename = resolveUrl(url.slice(1)) || "dummy"; + + fs.stat(possibleFilename, function(err, stats) { + var fileBuffer; + if (!err && stats.isFile()) { + fileBuffer = fs.readFileSync(possibleFilename); + let ct = mime.lookup(possibleFilename); + log(`Sending ${possibleFilename} with Content-Type ${ct}`); + res.writeHead(200, { 'Content-Type': ct }); + + } else { + log("Route %s, replacing with index.html", possibleFilename); + fileBuffer = returnDistFile(); + res.writeHead(200, { 'Content-Type': 'text/html' }); + } + + res.write(fileBuffer); + res.end(); + }); +} diff --git a/benchmarks/pairs/vfc-js/apex-publish-static-files-2-0-0/fixed.js b/benchmarks/pairs/vfc-js/apex-publish-static-files-2-0-0/fixed.js new file mode 100644 index 0000000..feebf68 --- /dev/null +++ b/benchmarks/pairs/vfc-js/apex-publish-static-files-2-0-0/fixed.js @@ -0,0 +1,95 @@ +// Provenance: vincentmorneau/apex-publish-static-files (fixed). +// repo: vincentmorneau/apex-publish-static-files +// commit: 2209af8f2b65c24aa55ab757e0e05b958c16f063 +// parent: 8d28fe8a9a49e5ff3f61e239293b5646528e2915 +// commit_url: https://github.com/vincentmorneau/apex-publish-static-files/commit/2209af8f2b65c24aa55ab757e0e05b958c16f063 +// cve: CVE-2018-16462 +// license: MIT +// function: publish +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 7 + +module.exports = { + // Validates a JSON object + publish(opts) { + // Set defaults + const defaults = { + sqlclPath: 'sql', + destination: 'application' + }; + opts = Object.assign(defaults, opts); + + // Validate arguments + if (typeof opts.connectString === 'undefined') { + throw new TypeError('connectString is required.'); + } + + if (typeof opts.directory === 'undefined') { + throw new TypeError('directory is a required argument.'); + } + + if (typeof opts.appID === 'undefined') { + throw new TypeError('appID is a required argument.'); + } + + if (opts.destination.toLowerCase() === 'plugin' && typeof opts.pluginName === 'undefined') { + throw new Error('pluginName is a required argument.'); + } + + if (!fs.existsSync(opts.directory)) { + throw new Error(`Directory ${opts.directory} is not a valid path.`); + } + + const getAllFiles = dir => + fs.readdirSync(dir).reduce((files, file) => { + const name = path.join(dir, file); + const isDirectory = fs.statSync(name).isDirectory(); + return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name]; + }, []); + + if (getAllFiles(opts.directory).length === 0) { + console.log(`Directory is empty.`); + } else { + // Execute the upload process + try { + switch (opts.destination.toLowerCase()) { + case 'theme': + console.log(`Uploading to ${opts.appID} - Theme Files...`); + break; + case 'workspace': + console.log(`Uploading to ${opts.appID} - Workspace Files...`); + break; + case 'plugin': + console.log(`Uploading to ${opts.appID} - ${opts.pluginName} - Plugin Files...`); + break; + default: + console.log(`Uploading to ${opts.appID} - Application Static Files...`); + } + + const spawnOpts = [ + opts.connectString, // Connect string (user/pass@server:port/sid) + '@' + path.resolve(__dirname, 'lib/script'), // Sql to execute + path.resolve(__dirname, 'lib/distUpload.js'), // Param &1 (js to execute) + path.resolve(opts.directory), // Param &2 + opts.appID, // Param &3 + opts.destination, // Param &4 + opts.pluginName // Param &5 + ]; + + const childProcess = spawnSync( + opts.sqlclPath, // Sqlcl path + spawnOpts, { + encoding: 'utf8' + } + ); + + console.log(childProcess.stdout); + console.log('Files were uploaded successfully.'); + } catch (err) { + console.error(err); + } + } + } +}; diff --git a/benchmarks/pairs/vfc-js/apex-publish-static-files-2-0-0/vuln.js b/benchmarks/pairs/vfc-js/apex-publish-static-files-2-0-0/vuln.js new file mode 100644 index 0000000..9a8a3e3 --- /dev/null +++ b/benchmarks/pairs/vfc-js/apex-publish-static-files-2-0-0/vuln.js @@ -0,0 +1,85 @@ +// Provenance: vincentmorneau/apex-publish-static-files (vuln). +// repo: vincentmorneau/apex-publish-static-files +// commit: 8d28fe8a9a49e5ff3f61e239293b5646528e2915 +// parent: 8d28fe8a9a49e5ff3f61e239293b5646528e2915 +// commit_url: https://github.com/vincentmorneau/apex-publish-static-files/commit/2209af8f2b65c24aa55ab757e0e05b958c16f063 +// cve: CVE-2018-16462 +// license: MIT +// function: publish +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 7 + +module.exports = { + // Validates a JSON object + publish(opts) { + // Set defaults + const defaults = { + sqlclPath: 'sql', + destination: 'application' + }; + opts = Object.assign(defaults, opts); + + // Validate arguments + if (typeof opts.connectString === 'undefined') { + throw new TypeError('connectString is required.'); + } + + if (typeof opts.directory === 'undefined') { + throw new TypeError('directory is a required argument.'); + } + + if (typeof opts.appID === 'undefined') { + throw new TypeError('appID is a required argument.'); + } + + if (opts.destination.toLowerCase() === 'plugin' && typeof opts.pluginName === 'undefined') { + throw new Error('pluginName is a required argument.'); + } + + if (!fs.existsSync(opts.directory)) { + throw new Error(`Directory ${opts.directory} is not a valid path.`); + } + + // Execute the upload process + try { + switch (opts.destination.toLowerCase()) { + case 'theme': + console.log(`Uploading to ${opts.appID} - Theme Files...`); + break; + case 'workspace': + console.log(`Uploading to ${opts.appID} - Workspace Files...`); + break; + case 'plugin': + console.log(`Uploading to ${opts.appID} - ${opts.pluginName} - Plugin Files...`); + break; + default: + console.log(`Uploading to ${opts.appID} - Application Static Files...`); + } + + const childProcess = execSync( + '"' + opts.sqlclPath + '"' + // Sqlcl path + ' ' + opts.connectString + // Connect string (user/pass@server:port/sid) + ' @"' + path.resolve(__dirname, 'lib/script') + '"' + // Sql to execute + ' "' + path.resolve(__dirname, 'lib/distUpload.js') + '"' + // Param &1 (js to execute) + ' "' + path.resolve(opts.directory) + '"' + // Param &2 + ' ' + opts.appID + // Param &3 + ' "' + opts.destination + '"' + // Param &4 + ' "' + opts.pluginName + '"' // Param &5 + , { + encoding: 'utf8' + } + ); + + console.log(childProcess); + + console.log('Files were uploaded successfully.'); + + return true; + } catch (err) { + console.error(err); + return false; + } + } +}; diff --git a/benchmarks/pairs/vfc-js/bestzip-2-1-6/fixed.js b/benchmarks/pairs/vfc-js/bestzip-2-1-6/fixed.js new file mode 100644 index 0000000..c2cf1a1 --- /dev/null +++ b/benchmarks/pairs/vfc-js/bestzip-2-1-6/fixed.js @@ -0,0 +1,43 @@ +// Provenance: nfriedly/node-bestzip (fixed). +// repo: nfriedly/node-bestzip +// commit: 45d4a901478c6a8f396c8b959dd6cf8fd3f955b6 +// parent: fcea252e52b6078eff6caf5531bd32c53fccb59f +// commit_url: https://github.com/nfriedly/node-bestzip/commit/45d4a901478c6a8f396c8b959dd6cf8fd3f955b6 +// cve: CVE-2020-7730 +// license: MIT +// function: nativeZip +// relpath: lib/bestzip.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 66 + +const nativeZip = options => + new Promise((resolve, reject) => { + const cwd = options.cwd || process.cwd(); + const command = "zip"; + expandSources(cwd, options.source, (err, sources) => { + const args = ["--quiet", "--recurse-paths", options.destination].concat( + sources + ); + const zipProcess = cp.spawn(command, args, { + stdio: "inherit", + cwd + }); + zipProcess.on("error", reject); + zipProcess.on("close", exitCode => { + if (exitCode === 0) { + resolve(); + } else { + // exit code 12 means "nothing to do" right? + //console.log('rejecting', zipProcess) + reject( + new Error( + `Unexpected exit code from native zip: ${exitCode}\n executed command '${command} ${args.join( + " " + )}'\n executed in directory '${cwd}'` + ) + ); + } + }); + }); + }); diff --git a/benchmarks/pairs/vfc-js/bestzip-2-1-6/vuln.js b/benchmarks/pairs/vfc-js/bestzip-2-1-6/vuln.js new file mode 100644 index 0000000..fd0a37a --- /dev/null +++ b/benchmarks/pairs/vfc-js/bestzip-2-1-6/vuln.js @@ -0,0 +1,41 @@ +// Provenance: nfriedly/node-bestzip (vuln). +// repo: nfriedly/node-bestzip +// commit: fcea252e52b6078eff6caf5531bd32c53fccb59f +// parent: fcea252e52b6078eff6caf5531bd32c53fccb59f +// commit_url: https://github.com/nfriedly/node-bestzip/commit/45d4a901478c6a8f396c8b959dd6cf8fd3f955b6 +// cve: CVE-2020-7730 +// license: MIT +// function: nativeZip +// relpath: lib/bestzip.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 66 + +const nativeZip = options => + new Promise((resolve, reject) => { + const sources = Array.isArray(options.source) + ? options.source.join(" ") + : options.source; + const command = `zip --quiet --recurse-paths ${ + options.destination + } ${sources}`; + const zipProcess = cp.exec(command, { + stdio: "inherit", + cwd: options.cwd + }); + zipProcess.on("error", reject); + zipProcess.on("close", exitCode => { + if (exitCode === 0) { + resolve(); + } else { + // exit code 12 means "nothing to do" right? + //console.log('rejecting', zipProcess) + reject( + new Error( + `Unexpected exit code from native zip command: ${exitCode}\n executed command '${command}'\n executed inin directory '${options.cwd || + process.cwd()}'` + ) + ); + } + }); + }); diff --git a/benchmarks/pairs/vfc-js/build_recipes.py b/benchmarks/pairs/vfc-js/build_recipes.py new file mode 100644 index 0000000..c3c9357 --- /dev/null +++ b/benchmarks/pairs/vfc-js/build_recipes.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Build vfc-js recipes from SecBench.js fix commits (maintainer, network via gh). + +SecBench.js has no license file, so nothing from it is vendored. Its package.json +metadata names the upstream fix commit; the pair is harvested from that upstream +repository (hunk mode: the function enclosing the first changed hunk) and carries +the upstream package license. Crawl first: + + python benchmarks/pairs/vfc-js/build_recipes.py --crawl /tmp/secbench.jsonl # lists modules with a GitHub fixCommit + python benchmarks/pairs/vfc-js/build_recipes.py --from /tmp/secbench.jsonl # resolves parent, license, changed file + python benchmarks/pairs/harvest.py --slice vfc-js --all --fetch + python benchmarks/pairs/catalog_gen.py --slice vfc-js +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +CLASSES = { + "command-injection": ("CWE-78", "command injection", "source_reaches_sink"), + "path-traversal": ("CWE-22", "path traversal", "path_join_user_input"), + "code-injection": ("CWE-94", "code injection", "source_reaches_sink"), + "prototype-pollution": ("CWE-1321", "prototype pollution", "prototype_pollution"), + "redos": ("CWE-1333", "regular expression denial of service", "redos"), +} +ACCEPTED_LICENSES = {"MIT", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "ISC", "MPL-2.0", "GPL-3.0", "GPL-2.0", "Unlicense", "0BSD"} +_JS = re.compile(r"\.(js|mjs|cjs|ts)$") +_SKIP = re.compile(r"(test|spec|\.d\.ts|dist/|build/|min\.js|package\.json|README)", re.I) + + +def _gh(path: str) -> object: + return json.loads(subprocess.check_output(["gh", "api", path], stderr=subprocess.DEVNULL)) + + +def crawl(out: Path, classes: tuple[str, ...]) -> int: + count = 0 + with out.open("w") as handle: + for cls in classes: + dirs = _gh(f"repos/cristianstaicu/SecBench.js/contents/{cls}") + for entry in dirs if isinstance(dirs, list) else []: + if entry.get("type") != "dir": + continue + time.sleep(0.4) + try: + raw = subprocess.check_output( + ["gh", "api", f"repos/cristianstaicu/SecBench.js/contents/{cls}/{entry['name']}/package.json", "--jq", ".content"], + stderr=subprocess.DEVNULL, + ) + meta = json.loads(base64.b64decode(raw)) + except Exception: # noqa: BLE001 — skip unreadable modules + continue + fix = str(meta.get("fixCommit", "n/a")) + if "github.com" in fix and "/commit/" in fix: + handle.write( + json.dumps( + {"class": cls, "module": entry["name"], "cve": meta.get("id"), "fixCommit": fix, "sink": meta.get("sink")} + ) + + "\n" + ) + count += 1 + return count + + +def build(crawl_path: Path, *, limit: int) -> tuple[list[dict[str, object]], dict[str, int]]: + recipes: list[dict[str, object]] = [] + skipped = {"unlicensed": 0, "no_js_file": 0, "api": 0, "multi_file": 0} + rows = [json.loads(line) for line in crawl_path.read_text().splitlines() if line.strip()] + seen_shas: set[str] = set() + seen_modules: set[str] = set() + skipped.update({"duplicate_fix": 0, "module_repo_mismatch": 0}) + for row in rows: + if len(recipes) >= limit: + break + match = re.search(r"github\.com/([^/]+/[^/]+)/commit/([0-9a-f]{7,40})", row["fixCommit"]) + if not match: + continue + repo, sha = match.group(1), match.group(2) + module_base = re.sub(r"[^a-z0-9]", "", str(row["module"]).rsplit("_", 1)[0].lower()) + repo_base = re.sub(r"[^a-z0-9]", "", repo.split("/")[-1].lower()) + if sha[:10] in seen_shas or str(row["module"]) in seen_modules: + skipped["duplicate_fix"] += 1 # SecBench.js records one upstream fix under several module names + continue + if module_base not in repo_base and repo_base not in module_base: + skipped["module_repo_mismatch"] += 1 # the fix commit belongs to another package; do not attribute + continue + seen_shas.add(sha[:10]) + seen_modules.add(str(row["module"])) + time.sleep(0.6) + try: + commit = _gh(f"repos/{repo}/commits/{sha}") + info = _gh(f"repos/{repo}") + except Exception: # noqa: BLE001 — upstream gone or renamed + skipped["api"] += 1 + continue + assert isinstance(commit, dict) and isinstance(info, dict) + license_id = (info.get("license") or {}).get("spdx_id") or "" + if license_id not in ACCEPTED_LICENSES: + skipped["unlicensed"] += 1 + continue + files = [f for f in commit.get("files", []) if _JS.search(f["filename"]) and not _SKIP.search(f["filename"]) and f.get("patch")] + if not files: + skipped["no_js_file"] += 1 + continue + if len(files) > 3: + skipped["multi_file"] += 1 + continue + target = sorted(files, key=lambda f: -(f.get("additions", 0) + f.get("deletions", 0)))[0] + parents = commit.get("parents") or [] + if not parents: + skipped["api"] += 1 + continue + cwe, cls_text, mechanism = CLASSES[row["class"]] + module = str(row["module"]) + name = f"{re.sub(r'[^a-z0-9]+', '-', module.lower()).strip('-')}"[:60] + recipes.append( + { + "name": name, + "host": "github", + "repo": repo, + "parent": parents[0]["sha"], + "commit": commit["sha"], + "path": target["filename"], + "mode": "hunk", + "language": "typescript" if target["filename"].endswith(".ts") else "javascript", + "relpath": target["filename"], + "license": license_id, + "cve": str(row.get("cve") or ""), + "year": int(str(commit["commit"]["author"]["date"])[:4]), + "cwe": cwe, + "class": cls_text, + "mechanism": mechanism, + "provenance": "human", + "origin": "secbench.js", + "project": module.rsplit("_", 1)[0], + "sink_location": str(row.get("sink") or ""), + "secbench_class": row["class"], + "evidence": f"SecBench.js {row['class']} sink {row.get('sink')} fixed upstream in {sha[:12]}", + "commit_url": f"https://github.com/{repo}/commit/{commit['sha']}", + } + ) + for index, recipe in enumerate(sorted(recipes, key=lambda r: str(r["name"]))): + recipe["split"] = "holdout" if index % 2 else "train" + return recipes, skipped + + +def to_toml(recipes: list[dict[str, object]]) -> str: + lines = [ + "# vfc-js harvest recipes derived from SecBench.js fixCommit metadata by build_recipes.py.", + "# Excerpts come from the upstream package repository (its license), never from SecBench.js.", + "", + ] + for recipe in sorted(recipes, key=lambda r: str(r["name"])): + lines.append("[[recipe]]") + for key, value in recipe.items(): + if isinstance(value, int) and not isinstance(value, bool): + lines.append(f"{key} = {value}") + else: + lines.append(f'{key} = "{str(value).replace(chr(92), chr(92) * 2).replace(chr(34), chr(92) + chr(34))}"') + lines.append("") + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--crawl", type=Path, help="write the SecBench.js module list with GitHub fix commits to this jsonl") + parser.add_argument("--classes", default="command-injection,path-traversal,code-injection") + parser.add_argument("--from", dest="source", type=Path, help="jsonl produced by --crawl") + parser.add_argument("--limit", type=int, default=40) + parser.add_argument("--out", type=Path, default=ROOT / "recipes.toml") + args = parser.parse_args(argv) + if args.crawl: + print(f"crawled {crawl(args.crawl, tuple(args.classes.split(',')))} modules -> {args.crawl}") + if args.source: + recipes, skipped = build(args.source, limit=args.limit) + args.out.write_text(to_toml(recipes), encoding="utf-8") + print(f"wrote {args.out} ({len(recipes)} recipes); skipped {skipped}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/pairs/vfc-js/catalog.toml b/benchmarks/pairs/vfc-js/catalog.toml new file mode 100644 index 0000000..146063d --- /dev/null +++ b/benchmarks/pairs/vfc-js/catalog.toml @@ -0,0 +1,493 @@ +# vfc-js pair slice. Reviewed isolated functions with provenance. +# Honesty dashboard — not the local 100% CI gate. +# Do not add these names to LANGUAGE_MANIFESTS. +# Regenerated by benchmarks/pairs/catalog_gen.py from recipes.toml. + +[[pair]] +name = "angular-http-server-1-0-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "Apache-2.0" +repo = "simonh1000/angular-http-server" +cve = "" +cwe = "CWE-22" +commit = "8bafc9577161469f5dea01e0b98ea9c525d063e9" +parent = "d171a771a92e4685b24fb77cb894a7b7b8024c5c" +commit_url = "https://github.com/simonh1000/angular-http-server/commit/8bafc9577161469f5dea01e0b98ea9c525d063e9" +vuln = "angular-http-server-1-0-0/vuln.js" +fixed = "angular-http-server-1-0-0/fixed.js" +relpath = "angular-http-server.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-22" +class = "path traversal" +path = "angular-http-server.js" +function = "requestListener" +mechanism = "path_join_user_input" +sink = "path" +evidence = "SecBench.js path-traversal sink angular-http-server.js:16:29 fixed upstream in 8bafc9577161" + +[[pair]] +name = "apex-publish-static-files-2-0-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "vincentmorneau/apex-publish-static-files" +cve = "CVE-2018-16462" +cwe = "CWE-78" +commit = "2209af8f2b65c24aa55ab757e0e05b958c16f063" +parent = "8d28fe8a9a49e5ff3f61e239293b5646528e2915" +commit_url = "https://github.com/vincentmorneau/apex-publish-static-files/commit/2209af8f2b65c24aa55ab757e0e05b958c16f063" +vuln = "apex-publish-static-files-2-0-0/vuln.js" +fixed = "apex-publish-static-files-2-0-0/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "index.js" +function = "publish" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink index.js:54:25 fixed upstream in 2209af8f2b65" + +[[pair]] +name = "bestzip-2-1-6" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "nfriedly/node-bestzip" +cve = "CVE-2020-7730" +cwe = "CWE-78" +commit = "45d4a901478c6a8f396c8b959dd6cf8fd3f955b6" +parent = "fcea252e52b6078eff6caf5531bd32c53fccb59f" +commit_url = "https://github.com/nfriedly/node-bestzip/commit/45d4a901478c6a8f396c8b959dd6cf8fd3f955b6" +vuln = "bestzip-2-1-6/vuln.js" +fixed = "bestzip-2-1-6/fixed.js" +relpath = "lib/bestzip.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "lib/bestzip.js" +function = "nativeZip" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink lib/bestzip.js:27:27 fixed upstream in 45d4a901478c" + +[[pair]] +name = "clamscan-1-2-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "kylefarris/clamscan" +cve = "CVE-2020-7613" +cwe = "CWE-78" +commit = "5f557c970817fe8c578ec3f7ad3bcbcef4cf5538" +parent = "d474da7b89c0d3397cd681e79c7bcc592bff144b" +commit_url = "https://github.com/kylefarris/clamscan/commit/5f557c970817fe8c578ec3f7ad3bcbcef4cf5538" +vuln = "clamscan-1-2-0/vuln.js" +fixed = "clamscan-1-2-0/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "index.js" +function = "local_scan" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink index.js:495:36 fixed upstream in 5f557c970817" + +[[pair]] +name = "diskusage-ng-0-2-6" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "iximiuz/node-diskusage-ng" +cve = "CVE-2020-7631" +cwe = "CWE-78" +commit = "48e7e093486b528f0c81ec699573e0e4a431b8d3" +parent = "53c505506553c83861d1bd67823d6a382d3a5a43" +commit_url = "https://github.com/iximiuz/node-diskusage-ng/commit/48e7e093486b528f0c81ec699573e0e4a431b8d3" +vuln = "diskusage-ng-0-2-6/vuln.js" +fixed = "diskusage-ng-0-2-6/fixed.js" +relpath = "lib/posix.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "lib/posix.js" +function = "diskusage" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink lib/posix.js:11:5 fixed upstream in 48e7e093486b" + +[[pair]] +name = "dns-sync-0-1-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "skoranga/node-dns-sync" +cve = "CVE-2014-9682" +cwe = "CWE-78" +commit = "d9abaae384b198db1095735ad9c1c73d7b890a0d" +parent = "b748fe77a87749d95876028eda8e759109be4c73" +commit_url = "https://github.com/skoranga/node-dns-sync/commit/d9abaae384b198db1095735ad9c1c73d7b890a0d" +vuln = "dns-sync-0-1-0/vuln.js" +fixed = "dns-sync-0-1-0/fixed.js" +relpath = "lib/dns-sync.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "lib/dns-sync.js" +function = "resolve" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink lib/dns-sync.js:21:26 fixed upstream in d9abaae384b1" + +[[pair]] +name = "glance-3-0-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "jarofghosts/glance" +cve = "CVE-2018-3715" +cwe = "CWE-22" +commit = "8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19" +parent = "7e69a5539236386e747fb240ae87ea65ed64bb2c" +commit_url = "https://github.com/jarofghosts/glance/commit/8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19" +vuln = "glance-3-0-0/vuln.js" +fixed = "glance-3-0-0/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-22" +class = "path traversal" +path = "index.js" +function = "Glance$serveRequest" +mechanism = "path_join_user_input" +evidence = "SecBench.js path-traversal sink index.js:81:14 fixed upstream in 8cfd88e44ebd" + +[[pair]] +name = "hot-formula-parser-3-0-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "handsontable/formula-parser" +cve = "CVE-2020-6836" +cwe = "CWE-78" +commit = "396b089738d4bf30eb570a4fe6a188affa95cd5e" +parent = "944136096f44ff4ffd4ba0bcef30970ca4702fce" +commit_url = "https://github.com/handsontable/formula-parser/commit/396b089738d4bf30eb570a4fe6a188affa95cd5e" +vuln = "hot-formula-parser-3-0-0/vuln.js" +fixed = "hot-formula-parser-3-0-0/fixed.js" +relpath = "src/grammar-parser/grammar-parser.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "src/grammar-parser/grammar-parser.js" +function = "performAction" +mechanism = "source_reaches_sink" +evidence = "SecBench.js command-injection sink lib/grammar-parser/grammar-parser.js:258:31 fixed upstream in 396b089738d4" + +[[pair]] +name = "libnmap-0-4-11" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "jas-/node-libnmap" +cve = "CVE-2018-16461" +cwe = "CWE-78" +commit = "c235b24b0342e12a5dbf502f89daabe95883b6c8" +parent = "49b72239b144ab7ffdf673b62e18bfd0760948f6" +commit_url = "https://github.com/jas-/node-libnmap/commit/c235b24b0342e12a5dbf502f89daabe95883b6c8" +vuln = "libnmap-0-4-11/vuln.js" +fixed = "libnmap-0-4-11/fixed.js" +relpath = "lib/classes/tools.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "lib/classes/tools.js" +function = "block" +mechanism = "source_reaches_sink" +evidence = "SecBench.js command-injection sink lib/classes/tools.js:114:25 fixed upstream in c235b24b0342" + +[[pair]] +name = "pdf-image-1-0-5" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "roest01/node-pdf-image" +cve = "CVE-2018-3757" +cwe = "CWE-78" +commit = "54679496a89738443917608c2bbe2f6e5dd20e83" +parent = "d8c0dcadd93985dbefe80b84060f5f572bfbb6ff" +commit_url = "https://github.com/roest01/node-pdf-image/commit/54679496a89738443917608c2bbe2f6e5dd20e83" +vuln = "pdf-image-1-0-5/vuln.js" +fixed = "pdf-image-1-0-5/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "index.js" +function = "getInfo" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink index.js:43:7 fixed upstream in 54679496a897" + +[[pair]] +name = "pidusage-1-0-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "soyuka/pidusage" +cve = "CVE-2017-16034" +cwe = "CWE-78" +commit = "b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d" +parent = "e3660d8a8e5e61afad9b02af67356a1e239d6bde" +commit_url = "https://github.com/soyuka/pidusage/commit/b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d" +vuln = "pidusage-1-0-0/vuln.js" +fixed = "pidusage-1-0-0/fixed.js" +relpath = "lib/stats.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "lib/stats.js" +function = "ps" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink lib/stats.js:108:5 fixed upstream in b70eca15f7ca" + +[[pair]] +name = "portprocesses-1-0-4" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "rrainn/PortProcesses" +cve = "CVE-2021-23348" +cwe = "CWE-78" +commit = "86811216c9b97b01b5722f879f8c88a7aa4214e1" +parent = "fffceb09aff7180afbd0bd172e820404b33c8299" +commit_url = "https://github.com/rrainn/PortProcesses/commit/86811216c9b97b01b5722f879f8c88a7aa4214e1" +vuln = "portprocesses-1-0-4/vuln.js" +fixed = "portprocesses-1-0-4/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "index.js" +function = "listProcessesOnPort" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink exec.js:5:3 fixed upstream in 86811216c9b9" + +[[pair]] +name = "serve-here-js-1-1-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "ChristoPy/serve-here.js" +cve = "CVE-2019-5444" +cwe = "CWE-22" +commit = "cefb51d03290b6a88dd13143ab2de31b8cf57c39" +parent = "ad077637bf7a9fe0dee3262f011aed19a45ce92c" +commit_url = "https://github.com/ChristoPy/serve-here.js/commit/cefb51d03290b6a88dd13143ab2de31b8cf57c39" +vuln = "serve-here-js-1-1-0/vuln.js" +fixed = "serve-here-js-1-1-0/fixed.js" +relpath = "source/response.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-22" +class = "path traversal" +path = "source/response.js" +function = "ConfigureFilePath" +mechanism = "path_join_user_input" +evidence = "SecBench.js path-traversal sink source/server.js:27:15 fixed upstream in cefb51d03290" + +[[pair]] +name = "static-eval-1-1-1" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "browserify/static-eval" +cve = "" +cwe = "CWE-94" +commit = "32f8efd072625e267aa7237d2daff35738c50d2a" +parent = "5bba7905b0e797460b8f6c2e38ff6869146faab3" +commit_url = "https://github.com/browserify/static-eval/commit/32f8efd072625e267aa7237d2daff35738c50d2a" +vuln = "static-eval-1-1-1/vuln.js" +fixed = "static-eval-1-1-1/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-94" +class = "code injection" +path = "index.js" +function = "walk" +mechanism = "source_reaches_sink" +sink = "eval" +evidence = "SecBench.js code-injection sink index.js:118:20 fixed upstream in 32f8efd07262" + +[[pair]] +name = "thenify-3-3-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "thenables/thenify" +cve = "CVE-2020-7677" +cwe = "CWE-94" +commit = "0d94a24eb933bc835d568f3009f4d269c4c4c17a" +parent = "aa21cfb64f47238c764d258d0faf7366a8487e36" +commit_url = "https://github.com/thenables/thenify/commit/0d94a24eb933bc835d568f3009f4d269c4c4c17a" +vuln = "thenify-3-3-0/vuln.js" +fixed = "thenify-3-3-0/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-94" +class = "code injection" +path = "index.js" +function = "thenify" +mechanism = "source_reaches_sink" +sink = "eval" +evidence = "SecBench.js code-injection sink index.js:17:10 fixed upstream in 0d94a24eb933" + +[[pair]] +name = "underscore-1-13-0-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "jashkenas/underscore" +cve = "CVE-2021-23358" +cwe = "CWE-94" +commit = "4c73526d43838ad6ab43a6134728776632adeb66" +parent = "ef646ccbbbd971e82719c83fc69f56a5585b3a01" +commit_url = "https://github.com/jashkenas/underscore/commit/4c73526d43838ad6ab43a6134728776632adeb66" +vuln = "underscore-1-13-0-0/vuln.js" +fixed = "underscore-1-13-0-0/fixed.js" +relpath = "modules/template.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-94" +class = "code injection" +path = "modules/template.js" +function = "template" +mechanism = "source_reaches_sink" +sink = "eval" +evidence = "SecBench.js code-injection sink modules/template.js:71:14 fixed upstream in 4c73526d4383" + +[[pair]] +name = "whereis-0-4-0" +slice = "vfc-js" +language = "javascript" +origin = "secbench.js" +license = "MIT" +repo = "vvo/node-whereis" +cve = "CVE-2018-3772" +cwe = "CWE-78" +commit = "0f64e3780235004fb6e43bfd153ea3e0e210ee2b" +parent = "b8b642b1163baa37f15ed49b4064dcbdb6e1876c" +commit_url = "https://github.com/vvo/node-whereis/commit/0f64e3780235004fb6e43bfd153ea3e0e210ee2b" +vuln = "whereis-0-4-0/vuln.js" +fixed = "whereis-0-4-0/fixed.js" +relpath = "index.js" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-78" +class = "command injection" +path = "index.js" +function = "whereis" +mechanism = "source_reaches_sink" +sink = "exec" +evidence = "SecBench.js command-injection sink index.js:4:6 fixed upstream in 0f64e3780235" diff --git a/benchmarks/pairs/vfc-js/clamscan-1-2-0/fixed.js b/benchmarks/pairs/vfc-js/clamscan-1-2-0/fixed.js new file mode 100644 index 0000000..92b2303 --- /dev/null +++ b/benchmarks/pairs/vfc-js/clamscan-1-2-0/fixed.js @@ -0,0 +1,1656 @@ +// Provenance: kylefarris/clamscan (fixed). +// repo: kylefarris/clamscan +// commit: 5f557c970817fe8c578ec3f7ad3bcbcef4cf5538 +// parent: d474da7b89c0d3397cd681e79c7bcc592bff144b +// commit_url: https://github.com/kylefarris/clamscan/commit/5f557c970817fe8c578ec3f7ad3bcbcef4cf5538 +// cve: CVE-2020-7613 +// license: MIT +// function: local_scan +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 69 + +class NodeClam { + constructor() { + this.initialized = false; + this.debug_label = 'node-clam'; + this.default_scanner = 'clamdscan'; + + // Configuration Settings + this.defaults = Object.freeze({ + remove_infected: false, + quarantine_infected: false, + scan_log: null, + debug_mode: false, + file_list: null, + scan_recursively: true, + clamscan: { + path: '/usr/bin/clamscan', + scan_archives: true, + db: null, + active: true + }, + clamdscan: { + socket: false, + host: false, + port: false, + timeout: 60000, // 60 seconds + local_fallback: true, + path: '/usr/bin/clamdscan', + config_file: null, + multiscan: true, + reload_db: false, + active: true, + bypass_test: false, + }, + preference: this.default_scanner + }); + + this.settings = Object.assign({}, this.defaults); + } + + // **************************************************************************** + // Initialize Method + // ----- + // @param Object options User options for the Clamscan module + // @param Function cb (optional) Callback method + // @return Promise If no callback is provided, a Promise is returned + // **************************************************************************** + async init(options={}, cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') { + throw new NodeClamError('Invalid cb provided to init method. Second paramter, if provided, must be a function!'); + } else if (cb && typeof cb === 'function') { + has_cb = true; + } + + return new Promise(async (resolve, reject) => { + + // No need to re-initialize + if (this.initialized === true) return (has_cb ? cb(null, this) : resolve(this)); + + // Override defaults with user preferences + let settings = {}; + if (Object.prototype.hasOwnProperty.call(options, 'clamscan') && Object.keys(options.clamscan).length > 0) { + settings.clamscan = Object.assign({}, this.defaults.clamscan, options.clamscan); + delete options.clamscan; + } + if (Object.prototype.hasOwnProperty.call(options, 'clamdscan') && Object.keys(options.clamdscan).length > 0) { + settings.clamdscan = Object.assign({}, this.defaults.clamdscan, options.clamdscan); + delete options.clamdscan; + } + this.settings = Object.assign({}, this.defaults, settings, options); + + if (this.settings && 'debug_mode' in this.settings && this.settings.debug_mode === true) + console.log(`${this.debug_label}: DEBUG MODE ON`); + + // Backwards compatibilty section + if ('quarantine_path' in this.settings && this.settings.quarantine_path) { + this.settings.quarantine_infected = this.settings.quarantine_path; + } + + // Determine whether to use clamdscan or clamscan + this.scanner = this.default_scanner; + + // If scanner preference is not defined or is invalid, fallback to streaming scan or completely fail + if (('preference' in this.settings && typeof this.settings.preference !== 'string') || !['clamscan','clamdscan'].includes(this.settings.preference)) { + // If no valid scanner is found (but a socket/host is), disable the fallback to a local CLI scanning method + if (this.settings.clamdscan.socket || this.settings.clamdscan.host) { + this.settings.clamdscan.local_fallback = false; + } else { + const err = new NodeClamError('Invalid virus scanner preference defined and no valid host/socket option provided!'); + return (has_cb ? cb(err, null) : reject(err)); + } + } + + // Set 'clamscan' as the scanner preference if it's specified as such and activated + // OR if 'clamdscan is the preference but inactivated and clamscan is activated + if ( + ( // If preference is 'clamscan' and clamscan is active + 'preference' in this.settings + && this.settings.preference === 'clamscan' + && 'clamscan' in this.settings + && 'active' in this.settings.clamscan + && this.settings.clamscan.active === true + ) + || // OR ... + ( // If preference is 'clamdscan' and it's NOT active but 'clamscan' is... + this.settings.preference === 'clamdscan' + && 'clamdscan' in this.settings + && 'active' in this.settings.clamdscan + && this.settings.clamdscan.active !== true + && 'clamscan' in this.settings + && 'active' in this.settings.clamscan + && this.settings.clamscan.active === true + ) + ) { + // Set scanner to clamscan + this.scanner = 'clamscan'; + } + + // Check to make sure preferred scanner exists and actually is a clamscan binary + try { + // If scanner binary doesn't exist... + if (!await this._is_clamav_binary(this.scanner)) { + // Fall back to other option: + if (this.scanner === 'clamdscan' && this.settings.clamscan.active === true && await this._is_clamav_binary('clamscan')) { + this.scanner = 'clamscan'; + } else if (this.scanner === 'clamscan' && this.settings.clamdscan.active === true && await this._is_clamav_binary('clamdscan')) { + this.scanner = 'clamdscan'; + } else { + // If preferred scanner is not a valid binary but there is a socket/host option, disable + // failover to local CLI implementation + if (this.settings.clamdscan.socket || this.settings.clamdscan.host) { + this.settings.clamdscan.local_fallback = false; + } else { + const err = new NodeClamError('No valid & active virus scanning binaries are active and available and host/socket option provided!'); + return (has_cb ? cb(err, null) : reject(err)); + } + } + } + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + + // Make sure quarantine_infected path exists at specified location + if ((!this.settings.clamdscan.socket && !this.settings.clamdscan.host && ((this.settings.clamdscan.active === true && this.settings.clamdscan.local_fallback === true) || (this.settings.clamscan.active === true))) && this.settings.quarantine_infected) { + try { + await fs_access(this.settings.quarantine_infected, fs.constants.R_OK); + } catch (e) { + if (this.settings.debug_mode) console.log(`${this.debug_label} error:`, err); + const err = new NodeClamError({err: e}, `Quarantine infected path (${this.settings.quarantine_infected}) is invalid.`); + return (has_cb ? cb(err, null) : reject(err)); + } + } + + // If using clamscan, make sure definition db exists at specified location + if (!this.settings.clamdscan.socket && !this.settings.clamdscan.host && this.scanner === 'clamscan' && this.settings.clamscan.db) { + try { + await fs_access(this.settings.clamscan.db, fs.constants.R_OK); + } catch (err) { + if (this.settings.debug_mode) console.log(`${this.debug_label} error:`, err); + //throw new Error(`Definitions DB path (${this.settings.clamscan.db}) is invalid.`); + this.settings.clamscan.db = null; + } + } + + // Make sure scan_log exists at specified location + if ( + ( + (!this.settings.clamdscan.socket && !this.settings.clamdscan.host) || + ( + (this.settings.clamdscan.socket || this.settings.clamdscan.host) && + this.settings.clamdscan.local_fallback === true && + this.settings.clamdscan.active === true + ) || + (this.settings.clamdscan.active === false && this.settings.clamscan.active === true) || + (this.preference) + ) && + this.settings.scan_log + ) { + try { + await fs_access(this.settings.scan_log, fs.constants.R_OK); + } catch (err) { + //console.log("DID NOT Find scan log!"); + //found_scan_log = false; + if (this.settings.debug_mode) console.log(`${this.debug_label} error:`, err); + //throw new Error(`Scan Log path (${this.settings.scan_log}) is invalid.` + err); + this.settings.scan_log = null; + } + } + + // Check the availability of the clamd service if socket or host/port are provided + if (this.scanner === 'clamdscan' && this.settings.clamdscan.bypass_test === false && (this.settings.clamdscan.socket || this.settings.clamdscan.host || this.settings.clamdscan.port)) { + if (this.settings.debug_mode) + console.log(`${this.debug_label}: Initially testing socket/tcp connection to clamscan server.`); + try { + await this._ping(); + if (this.settings.debug_mode) console.log(`${this.debug_label}: Established connection to clamscan server for testing!`); + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + } + + //if (found_scan_log === false) console.log("No Scan Log: ", this.settings); + + // Build clam flags + this.clam_flags = this._build_clam_flags(this.scanner, this.settings); + + //if (found_scan_log === false) console.log("No Scan Log: ", this.settings); + + // This ClamScan instance is now initialized + this.initialized = true; + + // Return instance based on type of expected response (callback vs promise) + return (has_cb ? cb(null, this) : resolve(this)); + }); + } + + // **************************************************************************** + // Allows one to create a new instances of clamscan with new options + // ----- + // + // **************************************************************************** + reset(options={}, cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') { + throw new NodeClamError('Invalid cb provided to `reset`. Second paramter, if provided, must be a function!'); + } else if (cb && typeof cb === 'function') { + has_cb = true; + } + + this.initialized = false; + this.settings = Object.assign({}, this.defaults); + + return new Promise(async (resolve, reject) => { + try { + await this.init(options); + return (has_cb ? cb(null, this) : resolve(this)); + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + }); + } + + // ***************************************************************************** + // Builds out the args to pass to execFile + // ----- + // @param String|Array item The file(s) / directory(ies) to append to the args + // @api Private + // ***************************************************************************** + _build_clam_args(item) { + let args = this.clam_flags.slice(); + + if (typeof item === 'string') args.push(item); + if (Array.isArray(item)) args = args.concat(item); + + return args; + } + + // ***************************************************************************** + // Builds out the flags based on the configuration the user provided + // ----- + // @access Private + // @param String scanner The scanner to use (clamscan or clamdscan) + // @param Object settings The settings used to build the flags + // @return String The concatenated clamav flags + // ***************************************************************************** + _build_clam_flags(scanner, settings) { + const flags_array = ['--no-summary']; + + // Flags specific to clamscan + if (scanner === 'clamscan') { + flags_array.push('--stdout'); + + // Remove infected files + if (settings.remove_infected === true) { + flags_array.push('--remove=yes'); + } else { + flags_array.push('--remove=no'); + } + + // Database file + if ('clamscan' in settings && typeof settings.clamscan === 'object' && 'db' in settings.clamscan && settings.clamscan.db && typeof settings.clamscan.db === 'string') + flags_array.push(`--database=${settings.clamscan.db}`); + + // Scan archives + if (settings.clamscan.scan_archives === true) { + flags_array.push('--scan-archive=yes'); + } else { + flags_array.push('--scan-archive=no'); + } + + // Recursive scanning (flag is specific, feature is not) + if (settings.scan_recursively === true) { + flags_array.push('-r'); + } else { + flags_array.push('--recursive=no'); + } + } + + // Flags specific to clamdscan + else if (scanner === 'clamdscan') { + flags_array.push('--fdpass'); + + // Remove infected files + if (settings.remove_infected === true) flags_array.push('--remove'); + + // Specify a config file + if ('clamdscan' in settings && typeof settings.clamdscan === 'object' && 'config_file' in settings.clamdscan && settings.clamdscan.config_file && typeof settings.clamdscan.config_file === 'string') + flags_array.push(`--config-file=${settings.clamdscan.config_file}`); + + // Turn on multi-threaded scanning + if (settings.clamdscan.multiscan === true) flags_array.push('--multiscan'); + + // Reload the virus DB + if (settings.clamdscan.reload_db === true) flags_array.push('--reload'); + } + + // *************** + // Common flags + // *************** + + // Remove infected files + if (settings.remove_infected !== true) { + if ('quarantine_infected' in settings && settings.quarantine_infected && typeof settings.quarantine_infected === 'string') + flags_array.push(`--move=${settings.quarantine_infected}`); + } + + // Write info to a log + if ('scan_log' in settings && settings.scan_log && typeof settings.scan_log === 'string') flags_array.push(`--log=${settings.scan_log}`); + + // Read list of files to scan from a file + if ('file_list' in settings && settings.file_list && typeof settings.file_list === 'string') flags_array.push(`--file-list=${settings.file_list}`); + + // Build the String + return flags_array; + } + + // **************************************************************************** + // Create socket connection to a remote (or local) clamav daemon. + // ----- + // @return Promise + // **************************************************************************** + _init_socket() { + return new Promise((resolve, reject) => { + // Create a new Socket connection to Unix socket or remote server (in that order) + let client; + + // The fastest option is a local Unix socket + if (this.settings.clamdscan.socket) + client = net.createConnection({path: this.settings.clamdscan.socket}); + + // If a port is specified, we're going to be connecting via TCP + else if (this.settings.clamdscan.port) { + // If a host is specified (usually for a remote host) + if (this.settings.clamdscan.host) { + client = net.createConnection({host: this.settings.clamdscan.host, port: this.settings.clamdscan.port}); + } + // Host can be ignored since the default is `localhost` + else { + client = net.createConnection({port: this.settings.clamdscan.port}); + } + } + + // No valid option to connection can be determined + else throw new NodeClamError('Unable not establish connection to clamd service: No socket or host/port combo provided!'); + + // Set the socket timeout if specified + if (this.settings.clamdscan.timeout) client.setTimeout(this.settings.clamdscan.timeout); + + // Setup socket client listeners + client + .on('connect', () => { + // Some basic debugging stuff... + // Determine information about what server the client is connected to + if (client.remotePort && client.remotePort.toString() === this.settings.clamdscan.port.toString()) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: using remote server: ${client.remoteAddress}:${client.remotePort}`); + } else if (this.settings.clamdscan.socket) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: using local unix domain socket: ${this.settings.clamdscan.socket}`); + } else { + if (this.settings.debug_mode) { + const {port, address} = client.address(); + console.log(`${this.debug_label}: meta port value: ${port} vs ${client.remotePort}`); + console.log(`${this.debug_label}: meta address value: ${address} vs ${client.remoteAddress}`); + console.log(`${this.debug_label}: something is not working...`); + } + } + + return resolve(client); + }) + .on('timeout', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Socket connection timed out.`); + client.end(); + }) + .on('close', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Socket connection closed.`); + }) + .on('error', (e) => { + reject(e); + }); + }); + } + + // **************************************************************************** + // Checks to see if a particular path contains a clamav binary + // ----- + // @param String scanner Scanner (clamscan or clamdscan) to check + // @return Promise + // **************************************************************************** + async _is_clamav_binary(scanner) { + const path = this.settings[scanner].path || null; + if (!path) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not determine path for clamav binary.`); + return false; + } + + const version_cmds = { + clamdscan: `${path} --version`, + clamscan: `${path} --version`, + }; + + try { + await fs_access(path, fs.constants.R_OK); + version_cmds_exec = version_cmds[scanner].split(' '); + const {stdout} = await cp_execfile(version_cmds_exec[0], [version_cmds_exec[1]]); + if (stdout.toString().match(/ClamAV/) === null) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`); + return false; + } + return true; + } catch (err) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`); + return false; + } + } + + // **************************************************************************** + // Really basic method to check if the configured `host` is actually the localhost + // machine. It's not flawless but a decently acurate check for our purposes. + // ----- + // @access Private + // @return Boolean TRUE: Is localhost; FALSE: is not localhost. + // **************************************************************************** + _is_localhost() { + return ['127.0.0.1','localhost', os.hostname()].includes(this.settings.clamdscan.host); + } + + // **************************************************************************** + // Test to see if ab object is a readable stream. + // ----- + // @access Private + // @param Object obj Object to test "streaminess" + // @return Boolean TRUE: Is stream; FALSE: is not stream. + // **************************************************************************** + _is_readable_stream(obj) { + if (!obj || typeof obj !== 'object') return false; + return typeof obj.pipe === 'function' && typeof obj._readableState === 'object'; + } + + // **************************************************************************** + // Quick check to see if the remote/local socket is working. Callback/Resolve + // response is an instance to a ClamAV socket client. + // ----- + // @access Private + // @param Function cb (optional) What to do after the ping + // @return Promise + // **************************************************************************** + _ping(cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') + throw new NodeClamError('Invalid cb provided to ping. Second parameter must be a function!'); + + // Making things simpler + if (cb && typeof cb === 'function') has_cb = true; + + return new Promise(async (resolve, reject) => { + try { + const client = await this._init_socket('test_availability'); + + if (this.settings.debug_mode) console.log(`${this.debug_label}: Established connection to clamscan server for testing!`); + + client.write('PING'); + client.on('data', data => { + if (data.toString().trim() === 'PONG') { + if (this.settings.debug_mode) console.log(`${this.debug_label}: PONG!`); + return (has_cb ? cb(null, client) : resolve(client)); + } + + // I'm not even sure this case is possible, but... + const err = new NodeClamError(data, 'Could not establish connection to the remote clamscan server.'); + return (has_cb ? cb(err, null) : reject(err)); + }); + } catch (err) { + return (has_cb ? cb(err, false) : resolve(err)); + } + }); + } + + // **************************************************************************** + // This is what actually processes the response from clamav + // ----- + // @access Private + // ----- + // @param String result The ClamAV result to process and interpret + // @param String file The name of the file/path that was scanned + // @return Object Object containing `is_infected` Boolean and `viruses` Array + // **************************************************************************** + _process_result(result, file=null) { + if (typeof result !== 'string') { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Invalid stdout from scanner (not a string): `, result); + throw new Error('Invalid result to process (not a string)'); + } + + result = result.trim(); + + // eslint-disable-next-line no-control-regex + if (/:\s+OK(\u0000|[\r\n])?$/.test(result)) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: File is OK!`); + return { is_infected: false, viruses: [], file, resultString: result }; + } + + // eslint-disable-next-line no-control-regex + if (/:\s+(.+)FOUND(\u0000|[\r\n])?/gm.test(result)) { + if (this.settings.debug_mode) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Scan Response: `, result); + if (this.settings.debug_mode) console.log(`${this.debug_label}: File is INFECTED!`); + } + + // Parse out the name of the virus(es) found... + // eslint-disable-next-line no-control-regex + const viruses = result.split(/(\u0000|[\r\n])/).map(v => /:\s+(.+)FOUND$/gm.test(v) ? v.replace(/(.+:\s+)(.+)FOUND/gm, '$2').trim() : null).filter(v => !!v); + + return { is_infected: true, viruses, file, resultString: result }; + } + + if (/^(.+)ERROR/gm.test(result)) { + const error = result.replace(/^(.+)ERROR/gm, '$1').trim(); + if (this.settings.debug_mode) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Error Response: `, error); + if (this.settings.debug_mode) console.log(`${this.debug_label}: File may be INFECTED!`); + } + return new NodeClamError({error}, `An error occurred while scanning the piped-through stream: ${error}`); + } + + if (this.settings.debug_mode) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Error Response: `, result); + if (this.settings.debug_mode) console.log(`${this.debug_label}: File may be INFECTED!`); + } + + return { is_infected: null, viruses: [], file, resultString: result }; + } + + // **************************************************************************** + // Establish the clamav version of a local or remote clamav daemon + // ----- + // @param Function cb (optional) What to do when version is established + // @return Promise + // **************************************************************************** + get_version(cb) { + const self = this; + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') + throw new NodeClamError('Invalid cb provided to scan_stream. Second paramter must be a function!'); + + // Making things simpler + if (cb && typeof cb === 'function') has_cb = true; + + return new Promise(async (resolve, reject) => { + // Function for falling back to running a scan locally via a child process + const local_fallback = async () => { + const command = `${self.settings[self.scanner].path} ${self._build_clam_args('--version').join(' ')}`; + + if (self.settings.debug_mode) { + console.log(`${this.debug_label}: Configured clam command: ${command}`); + } + + // Execute the clam binary with the proper flags + try { + const {stdout, stderr} = await cp_execfile(command); + + if (stderr) { + const err = new NodeClamError({stderr, file: null}, 'ClamAV responded with an unexpected response when requesting version.'); + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, err); + return (has_cb ? cb(err, null, null) : reject(err)); + } else { + return (has_cb ? cb(null, stdout) : resolve(stdout)); + } + } catch (e) { + if (Object.prototype.hasOwnProperty.call(e, 'code') && e.code === 1) { + return (has_cb ? cb(null, null) : resolve(null, null)); + } else { + const err = new NodeClamError({err: e}, 'There was an error requestion ClamAV version.'); + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, err); + return (has_cb ? cb(err, null) : reject(err)); + } + } + }; + + // If user wants to connect via socket or TCP... + if (this.scanner === 'clamdscan' && (this.settings.clamdscan.socket || this.settings.clamdscan.host)) { + const chunks = []; + + try { + const client = await this._init_socket(); + client.write('nVERSION\n'); + // ClamAV is sending stuff to us + client.on('data', chunk => chunks.push(chunk)); + client.on('end', () => { + const response = Buffer.concat(chunks); + return (has_cb ? cb(null, response.toString()) : resolve(response.toString())); + }); + } catch (err) { + if (this.settings.clamdscan.local_fallback === true) { + return local_fallback(); + } else { + return (has_cb ? cb(err, null) : resolve(err)); + } + } + } else { + return local_fallback(); + } + }); + } + + // **************************************************************************** + // Checks if a particular file is infected. + // ----- + // @param String file Path to the file to check + // @param Function cb (optional) What to do after the scan + // @return Promise + // **************************************************************************** + is_infected(file='', cb) { + const self = this; + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') { + throw new NodeClamError('Invalid cb provided to is_infected. Second paramter, if provided, must be a function!'); + } else if (cb && typeof cb === 'function') { + has_cb = true; + } + + // At this point for the hybrid Promise/CB API to work, everything needs to be wrapped + // in a Promise that will be returned + return new Promise(async (resolve, reject) => { + // Verify string is passed to the file parameter + if (typeof file !== 'string' || (typeof file === 'string' && file.trim() === '')) { + const err = new NodeClamError({file}, 'Invalid or empty file name provided.'); + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + // Clean file name + file = file.trim().replace(/ /g, ' '); + + // This is the function used for scanning viruses using the clamd command directly + const local_scan = () => { + //console.log("Doing local scan..."); + if (self.settings.debug_mode) console.log(`${this.debug_label}: Scanning ${file}`); + // Build the actual command to run + const args = self._build_clam_args(file); + if (self.settings.debug_mode) + console.log(`${this.debug_label}: Configured clam command: ${self.settings[self.scanner].path}`, args.join(' ')); + + // Execute the clam binary with the proper flags + // NOTE: The async/await version of this will not allow us to capture the virus(es) name(s). + execFile(self.settings[self.scanner].path, args, (err, stdout, stderr) => { + const {is_infected, viruses} = self._process_result(stdout, file); + + // It may be a real error or a virus may have been found. + if (err) { + // Code 1 is when a virus is found... It's not really an "error", per se... + if (Object.prototype.hasOwnProperty.call(err, 'code') && err.code === 1) { + return (has_cb ? cb(null, file, true, viruses) : resolve({file, is_infected, viruses})); + } else { + const error = new NodeClamError({file, err, is_infected: null}, `There was an error scanning the file (ClamAV Error Code: ${err.code})`); + if (self.settings.debug_mode) console.log(`${this.debug_label}`, error); + return (has_cb ? cb(error, file, null, []) : reject(error)); + } + } + // Not sure in what scenario a `stderr` would show up, but, it's worth handling here + else if (stderr) { + const err = new NodeClamError({stderr, file}, 'The file was scanned but ClamAV responded with an unexpected response.'); + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, err); + return (has_cb ? cb(err, file, null, viruses) : resolve({file, is_infected, viruses})); + } + // No viruses were found! + else { + try { + return (has_cb ? cb(null, file, is_infected, viruses) : resolve({file, is_infected, viruses})); + } catch (e) { + const err = new NodeClamError({file, err: e, is_infected: null}, 'There was an error processing the results from ClamAV'); + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + } + }); + }; + + // See if we can find/read the file + // ----- + // NOTE: Is it even valid to do this since, in theory, the + // file's existance or permission could change between this check + // and the actual scan (even if it's highly unlikely)? + //----- + try { + await fs_access(file, fs.constants.R_OK); + } catch (e) { + const err = new NodeClamError({err: e, file}, 'Could not find file to scan!'); + return (has_cb ? cb(err, file, true) : reject(err)); + } + // Make sure the "file" being scanned is actually a file and not a directory (or something else) + try { + const stats = await fs_stat(file); + const is_directory = stats.isDirectory(); + const is_file = stats.isFile(); + + // If it's not a file or a directory, fail now + if (!is_file && !is_directory) { + throw Error(`${file} is not a valid file or directory.`); + } + + // If it's a directory/path, scan it using the `scan_dir` method instead + else if (!is_file && is_directory) { + const {is_infected} = await this.scan_dir(file); + return (has_cb ? cb(null, file, is_infected, []) : resolve({file, is_infected, viruses: []})); + } + } catch (err) { + return (has_cb ? cb(err, file, null) : reject(err)); + } + + // If user wants to scan via socket or TCP... + if (this.settings.clamdscan.socket || this.settings.clamdscan.host) { + // console.log("Yep"); + // Scan using local unix domain socket (much simpler/faster process--especially with MULTISCAN enabled) + if (this.settings.clamdscan.socket) { + try { + const client = await this._init_socket('is_infected'); + if (this.settings.debug_mode) console.log(`${this.debug_label}: scanning with local domain socket now.`); + + if (this.settings.clamdscan.multiscan === true) { + // Use Multiple threads (faster) + client.write(`MULTISCAN ${file}`); + } else { + // Use single or default # of threads (potentially slower) + client.write(`SCAN ${file}`); + } + + client.on('data', async data => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Received response from remote clamd service.`); + try { + const result = this._process_result(data.toString(), file); + if (result instanceof Error) throw result; + + const {is_infected, viruses} = result; + return (has_cb ? cb(null, file, is_infected, viruses) : resolve({file, is_infected, viruses})); + } catch (err) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return await local_scan(); + + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + }); + } catch (err) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return await local_scan(); + + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + } + + // Scan using remote host/port and TCP protocol (must stream the file) + else { + // Convert file to stream + const stream = fs.createReadStream(file); + + // Attempt to scan the stream. + try { + const is_infected = await this.scan_stream(stream); + return (has_cb ? cb(null, file, is_infected, []) : resolve(Object.assign({file}, is_infected))); + } catch (e) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return await local_scan(); + + // Otherwise, fail + const err = new NodeClamError({err: e, file}, 'Could not scan file via TCP or locally!'); + return (has_cb ? cb(err, file, null, []) : reject(err)); + } finally { + // Kill file stream on response + stream.destroy(); + } + } + } + + + // If the user just wants to scan locally... + else { + try { + return await local_scan(); + } catch (err) { + return (has_cb ? cb(err, file, null) : reject(err)); + } + } + }); + } + + // **************************************************************************** + // This will return a Node Stream object that will allow a user to pass a stream + // THROUGH this module and on to something else whereby if a virus is detected + // mid-stream, the entire pipeline haults and an error event is emitted. + // ----- + // @param Stream stream A valid NodeJS stream object to pipe through ClamAV + // @return Stream A Transform stream + // **************************************************************************** + passthrough() { + const me = this; + // A chunk counter for debugging + let _scan_complete = false; + let _av_waiting = null; + let _av_scan_time = false; + + // DRY method for clearing the interval and counter related to scan times + const clear_scan_benchmark = () => { + if (_av_waiting) clearInterval(_av_waiting); + _av_waiting = null; + _av_scan_time = 0; + }; + + // Return a Transform stream so this can act as a "man-in-the-middle" + // for the streaming pipeline. + // Ex. upload_stream.pipe().pipe(destination_stream) + return new Transform({ + // This should be fired on each chunk received + async transform(chunk, encoding, cb) { + + // DRY method for handling each chunk as it comes in + const do_transform = () => { + // Write data to our fork stream. If it fails, + // emit a 'drain' event + if (!this._fork_stream.write(chunk)) { + this._fork_stream.once('drain', () => { + cb(null, chunk); + }); + } else { + // Push data back out to whatever is listening (if anything) + // and let Node know we're ready for more data + cb(null, chunk); + } + }; + + // DRY method for handling errors when the arise from the + // ClamAV Socket connection + const handle_error = (err, is_infected=null, result=null) => { + this._fork_stream.unpipe(); + this._fork_stream.destroy(); + this._clamav_transform.destroy(); + clear_scan_benchmark(); + + // Finding an infected file isn't really an error... + if (is_infected === true) { + if (_scan_complete === false) { + _scan_complete = true; + this.emit('scan-complete', result); + } + this.emit('stream-infected', result); // just another way to catch an infected stream + } else { + this.emit('error', err); + } + }; + + // If we haven't initialized a socket connection to ClamAV yet, + // now is the time... + if (!this._clamav_socket) { + // We're using a PassThrough stream as a middle man to fork the input + // into two paths... (1) ClamAV and (2) The final destination. + this._fork_stream = new PassThrough(); + // Instantiate our custom Transform stream that coddles + // chunks into the correct format for the ClamAV socket. + this._clamav_transform = new NodeClamTransform({}, me.settings.debug_mode); + // Setup an array to collect the responses from ClamAV + this._clamav_response_chunks = []; + + try { + // Get a connection to the ClamAV Socket + this._clamav_socket = await me._init_socket('passthrough'); + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV Socket Initialized...`); + + // Setup a pipeline that will pass chunks through our custom Tranform and on to ClamAV + this._fork_stream.pipe(this._clamav_transform).pipe(this._clamav_socket); + + // When the CLamAV socket connection is closed (could be after 'end' or because of an error)... + this._clamav_socket.on('close', hadError => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV socket has been closed! Because of Error:`, hadError); + }) + // When the ClamAV socket connection ends (receives chunk) + .on('end', () => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV socket has received the last chunk!`); + // Process the collected chunks + const response = Buffer.concat(this._clamav_response_chunks); + const result = me._process_result(response.toString('utf8'), null); + this._clamav_response_chunks = []; + if (me.settings.debug_mode) { + console.log(`${me.debug_label}: Result of scan:`, result); + console.log(`${me.debug_label}: It took ${_av_scan_time} seconds to scan the file(s).`); + clear_scan_benchmark(); + } + + // NOTE: "scan-complete" could be called by the `handle_error` method. + // We don't want to to double-emit this message. + if (_scan_complete === false) { + _scan_complete = true; + this.emit('scan-complete', result); + } + }) + // When the ClamAV socket is ready to receive packets (this will probably never fire here) + .on('ready', () => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV socket ready to receive`); + }) + // When we are officially connected to the ClamAV socket (probably will never fire here) + .on('connect', () => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: Connected to ClamAV socket`); + }) + // If an error is emitted from the ClamAV socket + .on('error', err => { + console.error(`${me.debug_label}: Error emitted from ClamAV socket: `, err); + handle_error(err); + }) + // If ClamAV is sending stuff to us (ie, an "OK", "Virus FOUND", or "ERROR") + .on('data', cv_chunk => { + // Push this chunk to our results collection array + this._clamav_response_chunks.push(cv_chunk); + if (me.settings.debug_mode) console.log(`${me.debug_label}: Got result!`, cv_chunk.toString()); + + // Parse what we've gotten back from ClamAV so far... + const response = Buffer.concat(this._clamav_response_chunks); + const result = me._process_result(response.toString(), null); + + // If there's an error supplied or if we detect a virus, stop stream immediately. + if (result instanceof NodeClamError || (typeof result === 'object' && 'is_infected' in result && result.is_infected === true)) { + // If a virus is detected... + if (typeof result === 'object' && 'is_infected' in result && result.is_infected === true) { + // handle_error(new NodeClamError(result, `Virus(es) found! ${'viruses' in result && Array.isArray(result.viruses) ? `Suspects: ${result.viruses.join(', ')}` : ''}`)); + handle_error(null, true, result); + } + // If any other kind of error is detected... + else { + handle_error(result); + } + } + // For debugging purposes, spit out what was processed (if anything). + else { + if (me.settings.debug_mode) console.log(`${me.debug_label}: Processed Result: `, result, response.toString()); + } + }); + + if (me.settings.debug_mode) console.log(`${me.debug_label}: Doing initial transform!`); + // Handle the chunk + do_transform(); + } catch (err) { + // If there's an issue connecting to the ClamAV socket, this is where that's handled + console.error(`${me.debug_label}: Error initiating socket to ClamAV: `, err); + } + } else { + //if (me.settings.debug_mode) console.log(`${me.debug_label}: Doing transform: ${++counter}`); + // Handle the chunk + do_transform(); + } + }, + + // This is what is called when the input stream has dried up + flush(cb) { + if (me.settings.debug_mode) console.log(`${me.debug_label}: Done with the full pipeline.`); + + // Keep track of how long it's taking to scan a file.. + _av_waiting = null; + _av_scan_time = 0; + if (me.settings.debug_mode) { + _av_waiting = setInterval(() => { + _av_scan_time += 1; + if (_av_scan_time % 5 === 0) console.log(`${me.debug_label}: ClamAV has been scanning for ${_av_scan_time} seconds...`); + }, 1000); + } + + // TODO: Investigate why this needs to be done in order + // for the ClamAV socket to be closed (why NodeClamTransform's + // `_flush` method isn't getting called) + // If the incoming stream is empty, transform() won't have been called, so we won't + // have a socket here. + if (this._clamav_socket && this._clamav_socket.writable === true) { + const size = Buffer.alloc(4); + size.writeInt32BE(0, 0); + this._clamav_socket.write(size, cb); + } + } + }); + } + + // **************************************************************************** + // Just an alias to `is_infected` + // **************************************************************************** + scan_file(file, cb) { + return this.is_infected(file, cb); + } + + // **************************************************************************** + // Scans an array of files or paths. You must provide the full paths of the + // files and/or paths. Also enables the ability to scan a file list. + // ----- + // @param Array files A list of files or paths (full paths) to be scanned. + // @param Function end_cb What to do after the scan + // @param Function file_cb What to do after each file has been scanned + // @return Promise + // **************************************************************************** + scan_files(files=[], end_cb=null, file_cb=null) { + const self = this; + let has_cb = false; + + // Verify third param, if supplied, is a function + if (file_cb && typeof file_cb !== 'function') + throw new NodeClamError('Invalid file callback provided to `scan_files`. Third paramter, if provided, must be a function!'); + + // Verify second param, if supplied, is a function + if (end_cb && typeof end_cb !== 'function') { + throw new NodeClamError('Invalid end-scan callback provided to `scan_files`. Second paramter, if provided, must be a function!'); + } else if (end_cb && typeof end_cb === 'function') { + has_cb = true; + } + + // We should probably have some reasonable limit on the number of files to scan + if (files && Array.isArray(files) && files.length > 1000000) + throw new NodeClamError({num_files: files.length}, 'NodeClam has haulted because more than 1 million files were about to be scanned. We suggest taking a different approach.'); + + // At this point for a hybrid Promise/CB API to work, everything needs to be wrapped + // in a Promise that will be returned + return new Promise(async (resolve, reject) => { + const errors = {}; + let bad_files = []; + let good_files = []; + let viruses = []; + let orig_num_files = 0; + + // The function that parses the stdout from clamscan/clamdscan + const parse_stdout = (err, stdout) => { + // Get Virus List + let viruses = stdout.trim().split(String.fromCharCode(10)).map(v => /FOUND\n?$/.test(v) ? v.replace(/(.+):\s+(.+)FOUND\n?$/, '$2').trim() : null).filter(v => !!v); + + stdout.trim() + .split(String.fromCharCode(10)) + .forEach(result => { + if (/^[-]+$/.test(result)) return; + + //console.log("PATH: " + result) + let path = result.match(/^(.*): /); + if (path && path.length > 0) { + path = path[1]; + } else { + path = ''; + } + + // eslint-disable-next-line no-control-regex + if (/\s+OK(\u0000|[\r\n])$/.test(result)) { + if (self.settings.debug_mode) console.log(`${this.debug_label}: ${path} is OK!`); + good_files.push(path); + } else { + if (self.settings.debug_mode) console.log(`${this.debug_label}: ${path} is INFECTED!`); + bad_files.push(path); + } + }); + + bad_files = Array.from(new Set(bad_files)); + good_files = Array.from(new Set(good_files)); + viruses = Array.from(new Set(viruses)); + + // return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + + if (err) return (has_cb ? end_cb(err, [], bad_files, {}, []) : reject(new NodeClamError({bad_files}, err))); + return (has_cb ? end_cb(null, good_files, bad_files, {}, viruses) : resolve({good_files, bad_files, viruses, errors: null})); + }; + + // Use this method when scanning using local binaries + const local_scan = async () => { + // Get array of escaped file names + const items = files.map(file => file.replace(/ /g,'\\ ')); + + // Build the actual command to run + const command = `${self.settings[self.scanner].path} ${self._build_clam_args(items).join(' ')}`; + if (self.settings.debug_mode) + if (self.settings.debug_mode) console.log(`${self.debug_label}: Configured clam command: ${command}}`); + + // Execute the clam binary with the proper flags + execFile(command, (err, stdout, stderr) => { + if (self.settings.debug_mode) console.log(`${this.debug_label}: stdout:`, stdout); + + if (err) return parse_stdout(err, stdout); + + if (stderr) { + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, stderr); + + if (stderr.length > 0) { + bad_files = stderr.split(os.EOL).map(err_line => { + const match = err_line.match(/^ERROR: Can't access file (.*)+$/); + if (match !== null && match.length > 1 && typeof match[1] === 'string') return match[1]; + return ''; + }); + + bad_files = bad_files.filter(v => !!v); + } + } + return parse_stdout(null, stdout); + }); + }; + + // This is the function that actually scans the files + const do_scan = async files => { + const num_files = files.length; + + if (self.settings.debug_mode) console.log(`${this.debug_label}: Scanning a list of ${num_files} passed files.`); + + // Slower but more verbose/informative way... + if (file_cb && typeof file_cb === 'function') { + // Scan files in parallel chunks of 10 + const chunk_size = 10; + let results = []; + while (files.length > 0) { + let chunk = []; + if (files.length > chunk_size) { + chunk = files.splice(0, chunk_size); + } else { + chunk = files.splice(0); + } + + // Scan 10 files then move to the next set... + const chunk_results = await Promise.all(chunk.map(file => this.is_infected(file).catch(e => e))); + + // Re-map results back to their filenames + const chunk_results_mapped = chunk_results.map((v,i) => [chunk[i], v]); + + // Trigger file-callback for each file that was just scanned + chunk_results_mapped.forEach(v => file_cb(null, v[0], v[1])); + + // Add mapped chunk results to overall scan results array + results = results.concat(chunk_results_mapped); + } + + // Build out the good and bad files arrays + results.forEach(v => { + if (v[1] === true) bad_files.push(v[0]); + else if (v[1] === false) good_files.push(v[0]); + else if (v[1] instanceof Error) { + errors[v[0]] = v[1]; + } + }); + + // Make sure the number of results matches the original number of files to be scanned + if (num_files !== results.length) { + const err_msg = 'The number of results did not match the number of files to scan!'; + return (has_cb ? end_cb(new NodeClamError(err_msg), good_files, bad_files, {}, []) : reject(new NodeClamError({good_files, bad_files}, err_msg))); + } + + // Make sure the list of bad and good files is unique...(just for good measure) + bad_files = Array.from(new Set(bad_files)); + good_files = Array.from(new Set(good_files)); + + if (self.settings.debug_mode) { + console.log(`${self.debug_label}: Scan Complete!`); + console.log(`${self.debug_label}: Num Bad Files: `, bad_files.length); + console.log(`${self.debug_label}: Num Good Files: `, good_files.length); + } + + return (has_cb ? end_cb(null, good_files, bad_files, {}, []) : resolve({good_files, bad_files, errors: null, viruses: []})); + } + + // The quicker but less-talkative way + else { + let all_files = []; + + // This is where we scan every file/path in the `all_files` array once it's been fully populated + const finish_scan = async () => { + // Make sure there are no dupes, falsy values, or non-strings... just because we can + all_files = Array.from(new Set(all_files.filter(v => !!v))).filter(v => typeof v === 'string'); + + // If file list is empty, return error + if (all_files.length <= 0) { + const err = new NodeClamError('No valid files provided to scan!'); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + + // If scanning via sockets, use that method, otherwise use `local_scan` + if (self.settings.clamdscan.socket || self.settings.clamdscan.port) { + const chunk_size = 10; + let results = []; + while (all_files.length > 0) { + let chunk = []; + if (all_files.length > chunk_size) { + chunk = all_files.splice(0, chunk_size); + } else { + chunk = all_files.splice(0); + } + + // Scan 10 files then move to the next set... + const chunk_results = await Promise.all(chunk.map(file => self.is_infected(file).catch(e => e))); + + // Re-map results back to their filenames + const chunk_results_mapped = chunk_results.map((v,i) => [chunk[i], v]); + //const chunk_results_mapped = chunk_results; + + // Add mapped chunk results to overall scan results array + results = results.concat(chunk_results_mapped); + } + + // Build out the good and bad files arrays + results.forEach(v => { + if (v[1] instanceof Error) errors[v[0]] = v[1]; + else if (typeof v[1] === 'object' && 'is_infected' in v[1] && v[1].is_infected === true) { + bad_files.push(v[1].file); + if ('viruses' in v[1] && Array.isArray(v[1].viruses) && v[1].viruses.length > 0) { + viruses = viruses.concat(v[1].viruses); + } + } + else if (typeof v[1] === 'object' && 'is_infected' in v[1] && v[1].is_infected === false) { + good_files.push(v[1].file); + } + }); + + // Make sure the list of bad and good files is unique...(just for good measure) + bad_files = Array.from(new Set(bad_files)); + good_files = Array.from(new Set(good_files)); + viruses = Array.from(new Set(viruses)); + + if (self.settings.debug_mode) { + console.log(`${self.debug_label}: Scan Complete!`); + console.log(`${self.debug_label}: Num Bad Files: `, bad_files.length); + console.log(`${self.debug_label}: Num Good Files: `, good_files.length); + console.log(`${self.debug_label}: Num Viruses: `, viruses.length); + } + + return (has_cb ? end_cb(null, good_files, bad_files, errors, viruses) : resolve({errors, good_files, bad_files, viruses})); + } else { + return local_scan(); + } + }; + + // If clamdscan is the preferred binary but we don't want to scan recursively + // then we need to convert all path entries to a list of files found in the + // first layer of that path + if (this.scan_recursively === false && this.scanner === 'clamdscan') { + const chunk_size = 10; + while (files.length > 0) { + let chunk = []; + if (files.length > chunk_size) { + chunk = files.splice(0, chunk_size); + } else { + chunk = files.splice(0); + } + + // Scan 10 files then move to the next set... + const chunk_results = await Promise.all(chunk.map(file => fs_stat(file).catch(e => e))); + + // Add each file to `all_files` array + // chunk_results.forEach(async (v,i) => { + for (let i in chunk_results) { + const v = chunk_results[i]; + // If the result is an error, add it to the error + // object and skip adding this file to the `all_files` array + if (v instanceof Error) { + errors[chunk[i]] = v; + } else if (v.isFile()) { + all_files.push(chunk[i]); + } else if (v.isDirectory()) { + const rgx = new RegExp(`^(?!${v})(.+)$`); + try { + const contents = (await fs_readdir(chunk[i], {withFileTypes: true})).filter(x => x.isFile()).map(x => x.name.replace(rgx, `${v}/${x.name}`)); + all_files = all_files.concat(contents); + } catch (e) { + errors[chunk[i]] = e; + } + } + } + + // Scan the files in the all_files array + return finish_scan(); + } + } else { + // Just scan all the files + all_files = files; + + // Scan the files in the all_files array + return finish_scan(); + } + } + }; + + // If string is provided in files param, forgive them... create a single element array + if (typeof files === 'string' && files.trim().length > 0) { + files = files.trim().split(',').map(v => v.trim()); + } + + // If the files array is actually an array, do some additional validation + if (Array.isArray(files)) { + // Keep track of the original number of files specified + orig_num_files = files.length; + + // Remove any empty or non-string elements + files = files.filter(v => !!v).filter(v => typeof v === 'string'); + + // If any items specified were not valid strings, fail... + if (files.length < orig_num_files) { + const err = new NodeClamError({num_files: files.length, orig_num_files}, 'You\'ve specified at least one invalid item to the files list (first parameter) of the `scan_files` method.'); + // console.log("Files: ", files); + // console.log("Num Files: ", files.length); + // console.log("Original Num Files: ", orig_num_files); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + } + + // Do some parameter validation + if (!Array.isArray(files) || files.length <= 0) { + // Before failing completely, check if there is a file list specified + if (!('file_list' in this.settings) || !this.settings.file_list) { + const err = new NodeClamError({files, settings: this.settings}, 'No files provided to scan and no file list provided!'); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + + // If the file list is specified, read it in and scan listed files... + try { + const data = (await fs_readfile(this.settings.file_list)).toString().split(os.EOL); + return do_scan(data); + } catch (e) { + const err = new NodeClamError({err: e, file_list: this.settings.file_list}, `No files provided and file list was provided but could not be found! ${e}`); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + } else { + return do_scan(files); + } + }); + } + + // **************************************************************************** + // Scans an entire directory. Provides 3 params to end callback: Error, path + // scanned, and whether its infected or not. To scan multiple directories, pass + // them as an array to the `scan_files` method. + // ----- + // NOTE: While possible, it is NOT advisable to use the file_cb parameter when + // using the clamscan binary. Doing so with clamdscan is okay, however. This + // method also allows for non-recursive scanning with the clamdscan binary. + // ----- + // @param String path The directory to scan files of + // @param Function end_cb (optional) What to do when all files have been scanned + // @param Function file_cb (optional) What to do after each file has been scanned + // @return Promise + // **************************************************************************** + scan_dir(path='', end_cb=null, file_cb=null) { + const self = this; + let has_cb = false; + + // Verify second param, if supplied, is a function + if (end_cb && typeof end_cb !== 'function') { + throw new NodeClamError('Invalid end-scan callback provided to `scan_dir`. Second paramter, if provided, must be a function!'); + } else if (end_cb && typeof end_cb === 'function') { + has_cb = true; + } + + // At this point for the hybrid Promise/CB API to work, everything needs to be wrapped + // in a Promise that will be returned + return new Promise(async (resolve, reject) => { + // Verify `path` provided is a string + if (typeof path !== 'string' || (typeof path === 'string' && path.trim() === '')) { + const err = new NodeClamError({path}, 'Invalid path provided! Path must be a string!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + + // Normalize and then trim trailing slash + path = node_path.normalize(path).replace(/\/$/, ''); + + // Make sure path exists... + try { + await fs_access(path, fs.constants.R_OK); + } catch (e) { + const err = new NodeClamError({path, err:e}, 'Invalid path specified to scan!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + + // Execute the clam binary with the proper flags + const local_scan = () => { + execFile(self.settings[self.scanner].path, self._build_clam_args(path), (err, stdout, stderr) => { + const {is_infected, viruses} = self._process_result(stdout, path); + + if (err) { + // Error code 1 means viruses were found... + if (Object.prototype.hasOwnProperty.call(err, 'code') && err.code === 1) { + return (has_cb ? end_cb(null, [], [path], viruses) : resolve({path, is_infected, bad_files: [path], good_files: [], viruses})); + } else { + const error = new NodeClamError({path, err}, 'There was an error scanning the path or processing the result.'); + return (has_cb ? end_cb(error, [], [], []) : reject(error)); + } + } + + if (stderr) { + console.error(`${self.debug_label} error: `, stderr); + return (has_cb ? end_cb(null, [], [], []) : resolve({stderr, path, is_infected, good_files: [], bad_files: [], viruses})); + } + + const good_files = (is_infected ? [] : [path]); + const bad_files = (is_infected ? [path] : []); + return (has_cb ? end_cb(null, good_files, bad_files, viruses) : resolve({path, is_infected, good_files, bad_files, viruses})); + }); + }; + + // Get all files recursively using `scan_files` + if (this.settings.scan_recursively === true && (typeof file_cb === 'function' || !has_cb)) { + try { + const {stdout, stderr} = await cp_execfile('find', [path]); + + if (stderr) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: `, stderr); + return (has_cb ? end_cb(null, [], []) : resolve({stderr, path, is_infected: null, good_files: [], bad_files: [], viruses: []})); + } + + const files = stdout.trim().split(os.EOL).map(path => path.replace(/ /g,'\\ ').trim()); + return this.scan_files(files, end_cb, file_cb); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'There was an issue scanning the path specified!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + // Clamdscan always does recursive, so, here's a way to avoid that if you want (will call `scan_files` method) + else if (this.settings.scan_recursively === false && this.scanner === 'clamdscan') { + try { + const all_files = (await fs_readdir(path)).filter(async file => (await fs_stat(file)).isFile()); + return this.scan_files(all_files, end_cb, file_cb); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'Could not read the file listing of the path provided.'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + + // If you don't care about individual file progress (which is very slow for clamscan but fine for clamdscan...) + // NOTE: This section WILL scan recursively + else if (typeof file_cb !== 'function' || !has_cb) { + // Scan locally via socket (either TCP or Unix socket) + // This is much simpler/faster process--potentially even more with MULTISCAN enabled) + if (this.settings.clamdscan.socket || (this.settings.clamdscan.port && this._is_localhost())) { + try { + const client = await this._init_socket(); + if (this.settings.debug_mode) console.log(`${this.debug_label}: scanning path with local domain socket now.`); + + if (this.settings.clamdscan.multiscan === true) { + // Use Multiple threads (faster) + client.write(`MULTISCAN ${path}`); + } else { + // Use single or default # of threads (potentially slower) + client.write(`SCAN ${path}`); + } + + // Where to buffer string response (not a real "Buffer", per se...) + const chunks = []; + + // Read output of the ClamAV socket to see what it's saying and when + // it's done saying it (FIN) + client + // ClamAV is sending stuff to us + .on('data', chunk => { + chunks.push(chunk); + }) + // ClamAV is done sending stuff to us + .on('end', async () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Received response from remote clamd service.`); + const response = Buffer.concat(chunks); + + const result = this._process_result(response.toString(), path); + if (result instanceof Error) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return local_scan(); + const err = new NodeClamError({path, err: result}, 'There was an issue scanning the path provided.'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + + const {is_infected, viruses} = result; + const good_files = (is_infected ? [] : [path]); + const bad_files = (is_infected ? [path] : []); + return (has_cb ? end_cb(null, good_files, bad_files, viruses) : resolve({path, is_infected, good_files, bad_files, viruses})); + }); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'There was an issue scanning the path provided.'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + + // Scan path recursively using remote host/port and TCP protocol (must stream every single file to it...) + // WARNING: This is going to be really slow + else if (this.settings.clamdscan.port && !this._is_localhost()) { + const results = []; + + try { + const {stdout, stderr} = await cp_execfile('find', [path]); + + if (stderr) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: `, stderr); + return (has_cb ? end_cb(null, [], []) : resolve({stderr, path, is_infected, good_files: [], bad_files: [], viruses: []})); + } + + // Get the proper recursive list of files from the path + const files = stdout.split('\n').map(path => path.replace(/ /g,'\\ ')); + + // Send files to remote server in parallel chunks of 10 + const chunk_size = 10; + while (files.length > 0) { + let chunk = []; + if (files.length > chunk_size) { + chunk = files.splice(0, chunk_size); + } else { + chunk = files.splice(0); + } + + // Scan 10 files then move to the next set... + results.concat(await Promise.all(chunk.map(file => this.scan_stream(fs.createReadStream(file))))); + } + + // If even a single file is infected, the whole directory is infected + const is_infected = results.any(v => v === false); + const good_files = (is_infected ? [] : [path]); + const bad_files = (is_infected ? [path] : []); + return (has_cb ? end_cb(null, good_files, bad_files) : resolve({path, is_infected, good_files, bad_files, viruses: []})); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'Invalid path provided! Path must be a string!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + + // Scan locally + else { + local_scan(); + } + } + }); + } + + // **************************************************************************** + // Scans a node Stream object + // ----- + // @param Stream stream The stream to scan + // @param Function callback (optional) What to do when the socket responds with results + // @return Promise + // **************************************************************************** + scan_stream(stream, cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') + throw new NodeClamError('Invalid cb provided to scan_stream. Second paramter must be a function!'); + + // Making things simpler + if (cb && typeof cb === 'function') has_cb = true; + + return new Promise(async (resolve, reject) => { + let finished = false; + + // Verify stream is passed to the first parameter + if (!this._is_readable_stream(stream)) { + const err = new NodeClamError({stream}, 'Invalid stream provided to scan.'); + return (has_cb ? cb(err, null) : reject(err)); + } else { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Provided stream is readable.`); + } + + // Verify that they have a valid socket or host/port config + if (!this.settings.clamdscan.socket && (!this.settings.clamdscan.port || !this.settings.clamdscan.host)) { + const err = new NodeClamError({clamdscan_settings: this.settings.clamdscan}, 'Invalid information provided to connect to clamav service. A unix socket or port (+ optional host) is required!'); + return (has_cb ? cb(err, null) : reject(err)); + } + + // Get a socket client + try { + // Get an instance of our stream tranform that coddles + // the chunks from the incoming stream to what ClamAV wants + const transform = new NodeClamTransform({}, this.settings.debug_mode); + + // Get a socket + const socket = await this._init_socket(); + + // Pipe the stream through our transform and into the ClamAV socket + stream.pipe(transform).pipe(socket); + + // Setup the listeners for the stream + stream + // The stream has dried up + .on('end', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: The input stream has dried up.`); + finished = true; + stream.destroy(); + }) + // There was an error with the stream (ex. uploader closed browser) + .on('error', err => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: There was an error with the input stream (maybe uploader closed browser?).`, err); + return (has_cb ? cb(err, null) : reject(err)); + }); + + + + // Where to buffer string response (not a real "Buffer", per se...) + const chunks = []; + + // Read output of the ClamAV socket to see what it's saying and when + // it's done saying it (FIN) + socket + // ClamAV is sending stuff to us + .on('data', chunk => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Received output from ClamAV Socket.`); + if (!stream.isPaused()) stream.pause(); + chunks.push(chunk); + }) + + .on('close', hadError => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: ClamAV socket has been closed!`, hadError); + }) + + .on('error', err => { + console.error(`${this.debug_label}: Error emitted from ClamAV socket: `, err); + return (has_cb ? cb(err, null) : reject(err)); + }) + + // ClamAV is done sending stuff to us + .on('end', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: ClamAV is done scanning.`); + const response = Buffer.concat(chunks); + if (!finished) { + const err = new NodeClamError('Scan aborted. Reply from server: ' + response.toString('utf8')); + return (has_cb ? cb(err, null) : reject(err)); + } else { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Raw Response: ${response.toString('utf8')}`); + const result = this._process_result(response.toString('utf8'), null); + return (has_cb ? cb(null, result) : resolve(result)); + } + }); + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + }); + } +} diff --git a/benchmarks/pairs/vfc-js/clamscan-1-2-0/vuln.js b/benchmarks/pairs/vfc-js/clamscan-1-2-0/vuln.js new file mode 100644 index 0000000..1805f86 --- /dev/null +++ b/benchmarks/pairs/vfc-js/clamscan-1-2-0/vuln.js @@ -0,0 +1,1656 @@ +// Provenance: kylefarris/clamscan (vuln). +// repo: kylefarris/clamscan +// commit: d474da7b89c0d3397cd681e79c7bcc592bff144b +// parent: d474da7b89c0d3397cd681e79c7bcc592bff144b +// commit_url: https://github.com/kylefarris/clamscan/commit/5f557c970817fe8c578ec3f7ad3bcbcef4cf5538 +// cve: CVE-2020-7613 +// license: MIT +// function: local_scan +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 69 + +class NodeClam { + constructor() { + this.initialized = false; + this.debug_label = 'node-clam'; + this.default_scanner = 'clamdscan'; + + // Configuration Settings + this.defaults = Object.freeze({ + remove_infected: false, + quarantine_infected: false, + scan_log: null, + debug_mode: false, + file_list: null, + scan_recursively: true, + clamscan: { + path: '/usr/bin/clamscan', + scan_archives: true, + db: null, + active: true + }, + clamdscan: { + socket: false, + host: false, + port: false, + timeout: 60000, // 60 seconds + local_fallback: true, + path: '/usr/bin/clamdscan', + config_file: null, + multiscan: true, + reload_db: false, + active: true, + bypass_test: false, + }, + preference: this.default_scanner + }); + + this.settings = Object.assign({}, this.defaults); + } + + // **************************************************************************** + // Initialize Method + // ----- + // @param Object options User options for the Clamscan module + // @param Function cb (optional) Callback method + // @return Promise If no callback is provided, a Promise is returned + // **************************************************************************** + async init(options={}, cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') { + throw new NodeClamError('Invalid cb provided to init method. Second paramter, if provided, must be a function!'); + } else if (cb && typeof cb === 'function') { + has_cb = true; + } + + return new Promise(async (resolve, reject) => { + + // No need to re-initialize + if (this.initialized === true) return (has_cb ? cb(null, this) : resolve(this)); + + // Override defaults with user preferences + let settings = {}; + if (Object.prototype.hasOwnProperty.call(options, 'clamscan') && Object.keys(options.clamscan).length > 0) { + settings.clamscan = Object.assign({}, this.defaults.clamscan, options.clamscan); + delete options.clamscan; + } + if (Object.prototype.hasOwnProperty.call(options, 'clamdscan') && Object.keys(options.clamdscan).length > 0) { + settings.clamdscan = Object.assign({}, this.defaults.clamdscan, options.clamdscan); + delete options.clamdscan; + } + this.settings = Object.assign({}, this.defaults, settings, options); + + if (this.settings && 'debug_mode' in this.settings && this.settings.debug_mode === true) + console.log(`${this.debug_label}: DEBUG MODE ON`); + + // Backwards compatibilty section + if ('quarantine_path' in this.settings && this.settings.quarantine_path) { + this.settings.quarantine_infected = this.settings.quarantine_path; + } + + // Determine whether to use clamdscan or clamscan + this.scanner = this.default_scanner; + + // If scanner preference is not defined or is invalid, fallback to streaming scan or completely fail + if (('preference' in this.settings && typeof this.settings.preference !== 'string') || !['clamscan','clamdscan'].includes(this.settings.preference)) { + // If no valid scanner is found (but a socket/host is), disable the fallback to a local CLI scanning method + if (this.settings.clamdscan.socket || this.settings.clamdscan.host) { + this.settings.clamdscan.local_fallback = false; + } else { + const err = new NodeClamError('Invalid virus scanner preference defined and no valid host/socket option provided!'); + return (has_cb ? cb(err, null) : reject(err)); + } + } + + // Set 'clamscan' as the scanner preference if it's specified as such and activated + // OR if 'clamdscan is the preference but inactivated and clamscan is activated + if ( + ( // If preference is 'clamscan' and clamscan is active + 'preference' in this.settings + && this.settings.preference === 'clamscan' + && 'clamscan' in this.settings + && 'active' in this.settings.clamscan + && this.settings.clamscan.active === true + ) + || // OR ... + ( // If preference is 'clamdscan' and it's NOT active but 'clamscan' is... + this.settings.preference === 'clamdscan' + && 'clamdscan' in this.settings + && 'active' in this.settings.clamdscan + && this.settings.clamdscan.active !== true + && 'clamscan' in this.settings + && 'active' in this.settings.clamscan + && this.settings.clamscan.active === true + ) + ) { + // Set scanner to clamscan + this.scanner = 'clamscan'; + } + + // Check to make sure preferred scanner exists and actually is a clamscan binary + try { + // If scanner binary doesn't exist... + if (!await this._is_clamav_binary(this.scanner)) { + // Fall back to other option: + if (this.scanner === 'clamdscan' && this.settings.clamscan.active === true && await this._is_clamav_binary('clamscan')) { + this.scanner = 'clamscan'; + } else if (this.scanner === 'clamscan' && this.settings.clamdscan.active === true && await this._is_clamav_binary('clamdscan')) { + this.scanner = 'clamdscan'; + } else { + // If preferred scanner is not a valid binary but there is a socket/host option, disable + // failover to local CLI implementation + if (this.settings.clamdscan.socket || this.settings.clamdscan.host) { + this.settings.clamdscan.local_fallback = false; + } else { + const err = new NodeClamError('No valid & active virus scanning binaries are active and available and host/socket option provided!'); + return (has_cb ? cb(err, null) : reject(err)); + } + } + } + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + + // Make sure quarantine_infected path exists at specified location + if ((!this.settings.clamdscan.socket && !this.settings.clamdscan.host && ((this.settings.clamdscan.active === true && this.settings.clamdscan.local_fallback === true) || (this.settings.clamscan.active === true))) && this.settings.quarantine_infected) { + try { + await fs_access(this.settings.quarantine_infected, fs.constants.R_OK); + } catch (e) { + if (this.settings.debug_mode) console.log(`${this.debug_label} error:`, err); + const err = new NodeClamError({err: e}, `Quarantine infected path (${this.settings.quarantine_infected}) is invalid.`); + return (has_cb ? cb(err, null) : reject(err)); + } + } + + // If using clamscan, make sure definition db exists at specified location + if (!this.settings.clamdscan.socket && !this.settings.clamdscan.host && this.scanner === 'clamscan' && this.settings.clamscan.db) { + try { + await fs_access(this.settings.clamscan.db, fs.constants.R_OK); + } catch (err) { + if (this.settings.debug_mode) console.log(`${this.debug_label} error:`, err); + //throw new Error(`Definitions DB path (${this.settings.clamscan.db}) is invalid.`); + this.settings.clamscan.db = null; + } + } + + // Make sure scan_log exists at specified location + if ( + ( + (!this.settings.clamdscan.socket && !this.settings.clamdscan.host) || + ( + (this.settings.clamdscan.socket || this.settings.clamdscan.host) && + this.settings.clamdscan.local_fallback === true && + this.settings.clamdscan.active === true + ) || + (this.settings.clamdscan.active === false && this.settings.clamscan.active === true) || + (this.preference) + ) && + this.settings.scan_log + ) { + try { + await fs_access(this.settings.scan_log, fs.constants.R_OK); + } catch (err) { + //console.log("DID NOT Find scan log!"); + //found_scan_log = false; + if (this.settings.debug_mode) console.log(`${this.debug_label} error:`, err); + //throw new Error(`Scan Log path (${this.settings.scan_log}) is invalid.` + err); + this.settings.scan_log = null; + } + } + + // Check the availability of the clamd service if socket or host/port are provided + if (this.scanner === 'clamdscan' && this.settings.clamdscan.bypass_test === false && (this.settings.clamdscan.socket || this.settings.clamdscan.host || this.settings.clamdscan.port)) { + if (this.settings.debug_mode) + console.log(`${this.debug_label}: Initially testing socket/tcp connection to clamscan server.`); + try { + await this._ping(); + if (this.settings.debug_mode) console.log(`${this.debug_label}: Established connection to clamscan server for testing!`); + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + } + + //if (found_scan_log === false) console.log("No Scan Log: ", this.settings); + + // Build clam flags + this.clam_flags = this._build_clam_flags(this.scanner, this.settings); + + //if (found_scan_log === false) console.log("No Scan Log: ", this.settings); + + // This ClamScan instance is now initialized + this.initialized = true; + + // Return instance based on type of expected response (callback vs promise) + return (has_cb ? cb(null, this) : resolve(this)); + }); + } + + // **************************************************************************** + // Allows one to create a new instances of clamscan with new options + // ----- + // + // **************************************************************************** + reset(options={}, cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') { + throw new NodeClamError('Invalid cb provided to `reset`. Second paramter, if provided, must be a function!'); + } else if (cb && typeof cb === 'function') { + has_cb = true; + } + + this.initialized = false; + this.settings = Object.assign({}, this.defaults); + + return new Promise(async (resolve, reject) => { + try { + await this.init(options); + return (has_cb ? cb(null, this) : resolve(this)); + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + }); + } + + // ***************************************************************************** + // Builds out the args to pass to execFile + // ----- + // @param String|Array item The file(s) / directory(ies) to append to the args + // @api Private + // ***************************************************************************** + _build_clam_args(item) { + let args = this.clam_flags.slice(); + + if (typeof item === 'string') args.push(item); + if (Array.isArray(item)) args = args.concat(item); + + return args; + } + + // ***************************************************************************** + // Builds out the flags based on the configuration the user provided + // ----- + // @access Private + // @param String scanner The scanner to use (clamscan or clamdscan) + // @param Object settings The settings used to build the flags + // @return String The concatenated clamav flags + // ***************************************************************************** + _build_clam_flags(scanner, settings) { + const flags_array = ['--no-summary']; + + // Flags specific to clamscan + if (scanner === 'clamscan') { + flags_array.push('--stdout'); + + // Remove infected files + if (settings.remove_infected === true) { + flags_array.push('--remove=yes'); + } else { + flags_array.push('--remove=no'); + } + + // Database file + if ('clamscan' in settings && typeof settings.clamscan === 'object' && 'db' in settings.clamscan && settings.clamscan.db && typeof settings.clamscan.db === 'string') + flags_array.push(`--database=${settings.clamscan.db}`); + + // Scan archives + if (settings.clamscan.scan_archives === true) { + flags_array.push('--scan-archive=yes'); + } else { + flags_array.push('--scan-archive=no'); + } + + // Recursive scanning (flag is specific, feature is not) + if (settings.scan_recursively === true) { + flags_array.push('-r'); + } else { + flags_array.push('--recursive=no'); + } + } + + // Flags specific to clamdscan + else if (scanner === 'clamdscan') { + flags_array.push('--fdpass'); + + // Remove infected files + if (settings.remove_infected === true) flags_array.push('--remove'); + + // Specify a config file + if ('clamdscan' in settings && typeof settings.clamdscan === 'object' && 'config_file' in settings.clamdscan && settings.clamdscan.config_file && typeof settings.clamdscan.config_file === 'string') + flags_array.push(`--config-file=${settings.clamdscan.config_file}`); + + // Turn on multi-threaded scanning + if (settings.clamdscan.multiscan === true) flags_array.push('--multiscan'); + + // Reload the virus DB + if (settings.clamdscan.reload_db === true) flags_array.push('--reload'); + } + + // *************** + // Common flags + // *************** + + // Remove infected files + if (settings.remove_infected !== true) { + if ('quarantine_infected' in settings && settings.quarantine_infected && typeof settings.quarantine_infected === 'string') + flags_array.push(`--move=${settings.quarantine_infected}`); + } + + // Write info to a log + if ('scan_log' in settings && settings.scan_log && typeof settings.scan_log === 'string') flags_array.push(`--log=${settings.scan_log}`); + + // Read list of files to scan from a file + if ('file_list' in settings && settings.file_list && typeof settings.file_list === 'string') flags_array.push(`--file-list=${settings.file_list}`); + + // Build the String + return flags_array; + } + + // **************************************************************************** + // Create socket connection to a remote (or local) clamav daemon. + // ----- + // @return Promise + // **************************************************************************** + _init_socket() { + return new Promise((resolve, reject) => { + // Create a new Socket connection to Unix socket or remote server (in that order) + let client; + + // The fastest option is a local Unix socket + if (this.settings.clamdscan.socket) + client = net.createConnection({path: this.settings.clamdscan.socket}); + + // If a port is specified, we're going to be connecting via TCP + else if (this.settings.clamdscan.port) { + // If a host is specified (usually for a remote host) + if (this.settings.clamdscan.host) { + client = net.createConnection({host: this.settings.clamdscan.host, port: this.settings.clamdscan.port}); + } + // Host can be ignored since the default is `localhost` + else { + client = net.createConnection({port: this.settings.clamdscan.port}); + } + } + + // No valid option to connection can be determined + else throw new NodeClamError('Unable not establish connection to clamd service: No socket or host/port combo provided!'); + + // Set the socket timeout if specified + if (this.settings.clamdscan.timeout) client.setTimeout(this.settings.clamdscan.timeout); + + // Setup socket client listeners + client + .on('connect', () => { + // Some basic debugging stuff... + // Determine information about what server the client is connected to + if (client.remotePort && client.remotePort.toString() === this.settings.clamdscan.port.toString()) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: using remote server: ${client.remoteAddress}:${client.remotePort}`); + } else if (this.settings.clamdscan.socket) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: using local unix domain socket: ${this.settings.clamdscan.socket}`); + } else { + if (this.settings.debug_mode) { + const {port, address} = client.address(); + console.log(`${this.debug_label}: meta port value: ${port} vs ${client.remotePort}`); + console.log(`${this.debug_label}: meta address value: ${address} vs ${client.remoteAddress}`); + console.log(`${this.debug_label}: something is not working...`); + } + } + + return resolve(client); + }) + .on('timeout', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Socket connection timed out.`); + client.end(); + }) + .on('close', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Socket connection closed.`); + }) + .on('error', (e) => { + reject(e); + }); + }); + } + + // **************************************************************************** + // Checks to see if a particular path contains a clamav binary + // ----- + // @param String scanner Scanner (clamscan or clamdscan) to check + // @return Promise + // **************************************************************************** + async _is_clamav_binary(scanner) { + const path = this.settings[scanner].path || null; + if (!path) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not determine path for clamav binary.`); + return false; + } + + const version_cmds = { + clamdscan: `${path} --version`, + clamscan: `${path} --version`, + }; + + try { + await fs_access(path, fs.constants.R_OK); + + const {stdout} = await cp_exec(version_cmds[scanner]); + if (stdout.toString().match(/ClamAV/) === null) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`); + return false; + } + return true; + } catch (err) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Could not verify the ${scanner} binary.`); + return false; + } + } + + // **************************************************************************** + // Really basic method to check if the configured `host` is actually the localhost + // machine. It's not flawless but a decently acurate check for our purposes. + // ----- + // @access Private + // @return Boolean TRUE: Is localhost; FALSE: is not localhost. + // **************************************************************************** + _is_localhost() { + return ['127.0.0.1','localhost', os.hostname()].includes(this.settings.clamdscan.host); + } + + // **************************************************************************** + // Test to see if ab object is a readable stream. + // ----- + // @access Private + // @param Object obj Object to test "streaminess" + // @return Boolean TRUE: Is stream; FALSE: is not stream. + // **************************************************************************** + _is_readable_stream(obj) { + if (!obj || typeof obj !== 'object') return false; + return typeof obj.pipe === 'function' && typeof obj._readableState === 'object'; + } + + // **************************************************************************** + // Quick check to see if the remote/local socket is working. Callback/Resolve + // response is an instance to a ClamAV socket client. + // ----- + // @access Private + // @param Function cb (optional) What to do after the ping + // @return Promise + // **************************************************************************** + _ping(cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') + throw new NodeClamError('Invalid cb provided to ping. Second parameter must be a function!'); + + // Making things simpler + if (cb && typeof cb === 'function') has_cb = true; + + return new Promise(async (resolve, reject) => { + try { + const client = await this._init_socket('test_availability'); + + if (this.settings.debug_mode) console.log(`${this.debug_label}: Established connection to clamscan server for testing!`); + + client.write('PING'); + client.on('data', data => { + if (data.toString().trim() === 'PONG') { + if (this.settings.debug_mode) console.log(`${this.debug_label}: PONG!`); + return (has_cb ? cb(null, client) : resolve(client)); + } + + // I'm not even sure this case is possible, but... + const err = new NodeClamError(data, 'Could not establish connection to the remote clamscan server.'); + return (has_cb ? cb(err, null) : reject(err)); + }); + } catch (err) { + return (has_cb ? cb(err, false) : resolve(err)); + } + }); + } + + // **************************************************************************** + // This is what actually processes the response from clamav + // ----- + // @access Private + // ----- + // @param String result The ClamAV result to process and interpret + // @param String file The name of the file/path that was scanned + // @return Object Object containing `is_infected` Boolean and `viruses` Array + // **************************************************************************** + _process_result(result, file=null) { + if (typeof result !== 'string') { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Invalid stdout from scanner (not a string): `, result); + throw new Error('Invalid result to process (not a string)'); + } + + result = result.trim(); + + // eslint-disable-next-line no-control-regex + if (/:\s+OK(\u0000|[\r\n])?$/.test(result)) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: File is OK!`); + return { is_infected: false, viruses: [], file, resultString: result }; + } + + // eslint-disable-next-line no-control-regex + if (/:\s+(.+)FOUND(\u0000|[\r\n])?/gm.test(result)) { + if (this.settings.debug_mode) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Scan Response: `, result); + if (this.settings.debug_mode) console.log(`${this.debug_label}: File is INFECTED!`); + } + + // Parse out the name of the virus(es) found... + // eslint-disable-next-line no-control-regex + const viruses = result.split(/(\u0000|[\r\n])/).map(v => /:\s+(.+)FOUND$/gm.test(v) ? v.replace(/(.+:\s+)(.+)FOUND/gm, '$2').trim() : null).filter(v => !!v); + + return { is_infected: true, viruses, file, resultString: result }; + } + + if (/^(.+)ERROR/gm.test(result)) { + const error = result.replace(/^(.+)ERROR/gm, '$1').trim(); + if (this.settings.debug_mode) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Error Response: `, error); + if (this.settings.debug_mode) console.log(`${this.debug_label}: File may be INFECTED!`); + } + return new NodeClamError({error}, `An error occurred while scanning the piped-through stream: ${error}`); + } + + if (this.settings.debug_mode) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Error Response: `, result); + if (this.settings.debug_mode) console.log(`${this.debug_label}: File may be INFECTED!`); + } + + return { is_infected: null, viruses: [], file, resultString: result }; + } + + // **************************************************************************** + // Establish the clamav version of a local or remote clamav daemon + // ----- + // @param Function cb (optional) What to do when version is established + // @return Promise + // **************************************************************************** + get_version(cb) { + const self = this; + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') + throw new NodeClamError('Invalid cb provided to scan_stream. Second paramter must be a function!'); + + // Making things simpler + if (cb && typeof cb === 'function') has_cb = true; + + return new Promise(async (resolve, reject) => { + // Function for falling back to running a scan locally via a child process + const local_fallback = async () => { + const command = `${self.settings[self.scanner].path} ${self._build_clam_args('--version').join(' ')}`; + + if (self.settings.debug_mode) { + console.log(`${this.debug_label}: Configured clam command: ${command}`); + } + + // Execute the clam binary with the proper flags + try { + const {stdout, stderr} = await cp_execfile(command); + + if (stderr) { + const err = new NodeClamError({stderr, file: null}, 'ClamAV responded with an unexpected response when requesting version.'); + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, err); + return (has_cb ? cb(err, null, null) : reject(err)); + } else { + return (has_cb ? cb(null, stdout) : resolve(stdout)); + } + } catch (e) { + if (Object.prototype.hasOwnProperty.call(e, 'code') && e.code === 1) { + return (has_cb ? cb(null, null) : resolve(null, null)); + } else { + const err = new NodeClamError({err: e}, 'There was an error requestion ClamAV version.'); + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, err); + return (has_cb ? cb(err, null) : reject(err)); + } + } + }; + + // If user wants to connect via socket or TCP... + if (this.scanner === 'clamdscan' && (this.settings.clamdscan.socket || this.settings.clamdscan.host)) { + const chunks = []; + + try { + const client = await this._init_socket(); + client.write('nVERSION\n'); + // ClamAV is sending stuff to us + client.on('data', chunk => chunks.push(chunk)); + client.on('end', () => { + const response = Buffer.concat(chunks); + return (has_cb ? cb(null, response.toString()) : resolve(response.toString())); + }); + } catch (err) { + if (this.settings.clamdscan.local_fallback === true) { + return local_fallback(); + } else { + return (has_cb ? cb(err, null) : resolve(err)); + } + } + } else { + return local_fallback(); + } + }); + } + + // **************************************************************************** + // Checks if a particular file is infected. + // ----- + // @param String file Path to the file to check + // @param Function cb (optional) What to do after the scan + // @return Promise + // **************************************************************************** + is_infected(file='', cb) { + const self = this; + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') { + throw new NodeClamError('Invalid cb provided to is_infected. Second paramter, if provided, must be a function!'); + } else if (cb && typeof cb === 'function') { + has_cb = true; + } + + // At this point for the hybrid Promise/CB API to work, everything needs to be wrapped + // in a Promise that will be returned + return new Promise(async (resolve, reject) => { + // Verify string is passed to the file parameter + if (typeof file !== 'string' || (typeof file === 'string' && file.trim() === '')) { + const err = new NodeClamError({file}, 'Invalid or empty file name provided.'); + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + // Clean file name + file = file.trim().replace(/ /g, ' '); + + // This is the function used for scanning viruses using the clamd command directly + const local_scan = () => { + //console.log("Doing local scan..."); + if (self.settings.debug_mode) console.log(`${this.debug_label}: Scanning ${file}`); + // Build the actual command to run + const args = self._build_clam_args(file); + if (self.settings.debug_mode) + console.log(`${this.debug_label}: Configured clam command: ${self.settings[self.scanner].path}`, args.join(' ')); + + // Execute the clam binary with the proper flags + // NOTE: The async/await version of this will not allow us to capture the virus(es) name(s). + execFile(self.settings[self.scanner].path, args, (err, stdout, stderr) => { + const {is_infected, viruses} = self._process_result(stdout, file); + + // It may be a real error or a virus may have been found. + if (err) { + // Code 1 is when a virus is found... It's not really an "error", per se... + if (Object.prototype.hasOwnProperty.call(err, 'code') && err.code === 1) { + return (has_cb ? cb(null, file, true, viruses) : resolve({file, is_infected, viruses})); + } else { + const error = new NodeClamError({file, err, is_infected: null}, `There was an error scanning the file (ClamAV Error Code: ${err.code})`); + if (self.settings.debug_mode) console.log(`${this.debug_label}`, error); + return (has_cb ? cb(error, file, null, []) : reject(error)); + } + } + // Not sure in what scenario a `stderr` would show up, but, it's worth handling here + else if (stderr) { + const err = new NodeClamError({stderr, file}, 'The file was scanned but ClamAV responded with an unexpected response.'); + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, err); + return (has_cb ? cb(err, file, null, viruses) : resolve({file, is_infected, viruses})); + } + // No viruses were found! + else { + try { + return (has_cb ? cb(null, file, is_infected, viruses) : resolve({file, is_infected, viruses})); + } catch (e) { + const err = new NodeClamError({file, err: e, is_infected: null}, 'There was an error processing the results from ClamAV'); + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + } + }); + }; + + // See if we can find/read the file + // ----- + // NOTE: Is it even valid to do this since, in theory, the + // file's existance or permission could change between this check + // and the actual scan (even if it's highly unlikely)? + //----- + try { + await fs_access(file, fs.constants.R_OK); + } catch (e) { + const err = new NodeClamError({err: e, file}, 'Could not find file to scan!'); + return (has_cb ? cb(err, file, true) : reject(err)); + } + // Make sure the "file" being scanned is actually a file and not a directory (or something else) + try { + const stats = await fs_stat(file); + const is_directory = stats.isDirectory(); + const is_file = stats.isFile(); + + // If it's not a file or a directory, fail now + if (!is_file && !is_directory) { + throw Error(`${file} is not a valid file or directory.`); + } + + // If it's a directory/path, scan it using the `scan_dir` method instead + else if (!is_file && is_directory) { + const {is_infected} = await this.scan_dir(file); + return (has_cb ? cb(null, file, is_infected, []) : resolve({file, is_infected, viruses: []})); + } + } catch (err) { + return (has_cb ? cb(err, file, null) : reject(err)); + } + + // If user wants to scan via socket or TCP... + if (this.settings.clamdscan.socket || this.settings.clamdscan.host) { + // console.log("Yep"); + // Scan using local unix domain socket (much simpler/faster process--especially with MULTISCAN enabled) + if (this.settings.clamdscan.socket) { + try { + const client = await this._init_socket('is_infected'); + if (this.settings.debug_mode) console.log(`${this.debug_label}: scanning with local domain socket now.`); + + if (this.settings.clamdscan.multiscan === true) { + // Use Multiple threads (faster) + client.write(`MULTISCAN ${file}`); + } else { + // Use single or default # of threads (potentially slower) + client.write(`SCAN ${file}`); + } + + client.on('data', async data => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Received response from remote clamd service.`); + try { + const result = this._process_result(data.toString(), file); + if (result instanceof Error) throw result; + + const {is_infected, viruses} = result; + return (has_cb ? cb(null, file, is_infected, viruses) : resolve({file, is_infected, viruses})); + } catch (err) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return await local_scan(); + + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + }); + } catch (err) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return await local_scan(); + + return (has_cb ? cb(err, file, null, []) : reject(err)); + } + } + + // Scan using remote host/port and TCP protocol (must stream the file) + else { + // Convert file to stream + const stream = fs.createReadStream(file); + + // Attempt to scan the stream. + try { + const is_infected = await this.scan_stream(stream); + return (has_cb ? cb(null, file, is_infected, []) : resolve(Object.assign({file}, is_infected))); + } catch (e) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return await local_scan(); + + // Otherwise, fail + const err = new NodeClamError({err: e, file}, 'Could not scan file via TCP or locally!'); + return (has_cb ? cb(err, file, null, []) : reject(err)); + } finally { + // Kill file stream on response + stream.destroy(); + } + } + } + + + // If the user just wants to scan locally... + else { + try { + return await local_scan(); + } catch (err) { + return (has_cb ? cb(err, file, null) : reject(err)); + } + } + }); + } + + // **************************************************************************** + // This will return a Node Stream object that will allow a user to pass a stream + // THROUGH this module and on to something else whereby if a virus is detected + // mid-stream, the entire pipeline haults and an error event is emitted. + // ----- + // @param Stream stream A valid NodeJS stream object to pipe through ClamAV + // @return Stream A Transform stream + // **************************************************************************** + passthrough() { + const me = this; + // A chunk counter for debugging + let _scan_complete = false; + let _av_waiting = null; + let _av_scan_time = false; + + // DRY method for clearing the interval and counter related to scan times + const clear_scan_benchmark = () => { + if (_av_waiting) clearInterval(_av_waiting); + _av_waiting = null; + _av_scan_time = 0; + }; + + // Return a Transform stream so this can act as a "man-in-the-middle" + // for the streaming pipeline. + // Ex. upload_stream.pipe().pipe(destination_stream) + return new Transform({ + // This should be fired on each chunk received + async transform(chunk, encoding, cb) { + + // DRY method for handling each chunk as it comes in + const do_transform = () => { + // Write data to our fork stream. If it fails, + // emit a 'drain' event + if (!this._fork_stream.write(chunk)) { + this._fork_stream.once('drain', () => { + cb(null, chunk); + }); + } else { + // Push data back out to whatever is listening (if anything) + // and let Node know we're ready for more data + cb(null, chunk); + } + }; + + // DRY method for handling errors when the arise from the + // ClamAV Socket connection + const handle_error = (err, is_infected=null, result=null) => { + this._fork_stream.unpipe(); + this._fork_stream.destroy(); + this._clamav_transform.destroy(); + clear_scan_benchmark(); + + // Finding an infected file isn't really an error... + if (is_infected === true) { + if (_scan_complete === false) { + _scan_complete = true; + this.emit('scan-complete', result); + } + this.emit('stream-infected', result); // just another way to catch an infected stream + } else { + this.emit('error', err); + } + }; + + // If we haven't initialized a socket connection to ClamAV yet, + // now is the time... + if (!this._clamav_socket) { + // We're using a PassThrough stream as a middle man to fork the input + // into two paths... (1) ClamAV and (2) The final destination. + this._fork_stream = new PassThrough(); + // Instantiate our custom Transform stream that coddles + // chunks into the correct format for the ClamAV socket. + this._clamav_transform = new NodeClamTransform({}, me.settings.debug_mode); + // Setup an array to collect the responses from ClamAV + this._clamav_response_chunks = []; + + try { + // Get a connection to the ClamAV Socket + this._clamav_socket = await me._init_socket('passthrough'); + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV Socket Initialized...`); + + // Setup a pipeline that will pass chunks through our custom Tranform and on to ClamAV + this._fork_stream.pipe(this._clamav_transform).pipe(this._clamav_socket); + + // When the CLamAV socket connection is closed (could be after 'end' or because of an error)... + this._clamav_socket.on('close', hadError => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV socket has been closed! Because of Error:`, hadError); + }) + // When the ClamAV socket connection ends (receives chunk) + .on('end', () => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV socket has received the last chunk!`); + // Process the collected chunks + const response = Buffer.concat(this._clamav_response_chunks); + const result = me._process_result(response.toString('utf8'), null); + this._clamav_response_chunks = []; + if (me.settings.debug_mode) { + console.log(`${me.debug_label}: Result of scan:`, result); + console.log(`${me.debug_label}: It took ${_av_scan_time} seconds to scan the file(s).`); + clear_scan_benchmark(); + } + + // NOTE: "scan-complete" could be called by the `handle_error` method. + // We don't want to to double-emit this message. + if (_scan_complete === false) { + _scan_complete = true; + this.emit('scan-complete', result); + } + }) + // When the ClamAV socket is ready to receive packets (this will probably never fire here) + .on('ready', () => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: ClamAV socket ready to receive`); + }) + // When we are officially connected to the ClamAV socket (probably will never fire here) + .on('connect', () => { + if (me.settings.debug_mode) console.log(`${me.debug_label}: Connected to ClamAV socket`); + }) + // If an error is emitted from the ClamAV socket + .on('error', err => { + console.error(`${me.debug_label}: Error emitted from ClamAV socket: `, err); + handle_error(err); + }) + // If ClamAV is sending stuff to us (ie, an "OK", "Virus FOUND", or "ERROR") + .on('data', cv_chunk => { + // Push this chunk to our results collection array + this._clamav_response_chunks.push(cv_chunk); + if (me.settings.debug_mode) console.log(`${me.debug_label}: Got result!`, cv_chunk.toString()); + + // Parse what we've gotten back from ClamAV so far... + const response = Buffer.concat(this._clamav_response_chunks); + const result = me._process_result(response.toString(), null); + + // If there's an error supplied or if we detect a virus, stop stream immediately. + if (result instanceof NodeClamError || (typeof result === 'object' && 'is_infected' in result && result.is_infected === true)) { + // If a virus is detected... + if (typeof result === 'object' && 'is_infected' in result && result.is_infected === true) { + // handle_error(new NodeClamError(result, `Virus(es) found! ${'viruses' in result && Array.isArray(result.viruses) ? `Suspects: ${result.viruses.join(', ')}` : ''}`)); + handle_error(null, true, result); + } + // If any other kind of error is detected... + else { + handle_error(result); + } + } + // For debugging purposes, spit out what was processed (if anything). + else { + if (me.settings.debug_mode) console.log(`${me.debug_label}: Processed Result: `, result, response.toString()); + } + }); + + if (me.settings.debug_mode) console.log(`${me.debug_label}: Doing initial transform!`); + // Handle the chunk + do_transform(); + } catch (err) { + // If there's an issue connecting to the ClamAV socket, this is where that's handled + console.error(`${me.debug_label}: Error initiating socket to ClamAV: `, err); + } + } else { + //if (me.settings.debug_mode) console.log(`${me.debug_label}: Doing transform: ${++counter}`); + // Handle the chunk + do_transform(); + } + }, + + // This is what is called when the input stream has dried up + flush(cb) { + if (me.settings.debug_mode) console.log(`${me.debug_label}: Done with the full pipeline.`); + + // Keep track of how long it's taking to scan a file.. + _av_waiting = null; + _av_scan_time = 0; + if (me.settings.debug_mode) { + _av_waiting = setInterval(() => { + _av_scan_time += 1; + if (_av_scan_time % 5 === 0) console.log(`${me.debug_label}: ClamAV has been scanning for ${_av_scan_time} seconds...`); + }, 1000); + } + + // TODO: Investigate why this needs to be done in order + // for the ClamAV socket to be closed (why NodeClamTransform's + // `_flush` method isn't getting called) + // If the incoming stream is empty, transform() won't have been called, so we won't + // have a socket here. + if (this._clamav_socket && this._clamav_socket.writable === true) { + const size = Buffer.alloc(4); + size.writeInt32BE(0, 0); + this._clamav_socket.write(size, cb); + } + } + }); + } + + // **************************************************************************** + // Just an alias to `is_infected` + // **************************************************************************** + scan_file(file, cb) { + return this.is_infected(file, cb); + } + + // **************************************************************************** + // Scans an array of files or paths. You must provide the full paths of the + // files and/or paths. Also enables the ability to scan a file list. + // ----- + // @param Array files A list of files or paths (full paths) to be scanned. + // @param Function end_cb What to do after the scan + // @param Function file_cb What to do after each file has been scanned + // @return Promise + // **************************************************************************** + scan_files(files=[], end_cb=null, file_cb=null) { + const self = this; + let has_cb = false; + + // Verify third param, if supplied, is a function + if (file_cb && typeof file_cb !== 'function') + throw new NodeClamError('Invalid file callback provided to `scan_files`. Third paramter, if provided, must be a function!'); + + // Verify second param, if supplied, is a function + if (end_cb && typeof end_cb !== 'function') { + throw new NodeClamError('Invalid end-scan callback provided to `scan_files`. Second paramter, if provided, must be a function!'); + } else if (end_cb && typeof end_cb === 'function') { + has_cb = true; + } + + // We should probably have some reasonable limit on the number of files to scan + if (files && Array.isArray(files) && files.length > 1000000) + throw new NodeClamError({num_files: files.length}, 'NodeClam has haulted because more than 1 million files were about to be scanned. We suggest taking a different approach.'); + + // At this point for a hybrid Promise/CB API to work, everything needs to be wrapped + // in a Promise that will be returned + return new Promise(async (resolve, reject) => { + const errors = {}; + let bad_files = []; + let good_files = []; + let viruses = []; + let orig_num_files = 0; + + // The function that parses the stdout from clamscan/clamdscan + const parse_stdout = (err, stdout) => { + // Get Virus List + let viruses = stdout.trim().split(String.fromCharCode(10)).map(v => /FOUND\n?$/.test(v) ? v.replace(/(.+):\s+(.+)FOUND\n?$/, '$2').trim() : null).filter(v => !!v); + + stdout.trim() + .split(String.fromCharCode(10)) + .forEach(result => { + if (/^[-]+$/.test(result)) return; + + //console.log("PATH: " + result) + let path = result.match(/^(.*): /); + if (path && path.length > 0) { + path = path[1]; + } else { + path = ''; + } + + // eslint-disable-next-line no-control-regex + if (/\s+OK(\u0000|[\r\n])$/.test(result)) { + if (self.settings.debug_mode) console.log(`${this.debug_label}: ${path} is OK!`); + good_files.push(path); + } else { + if (self.settings.debug_mode) console.log(`${this.debug_label}: ${path} is INFECTED!`); + bad_files.push(path); + } + }); + + bad_files = Array.from(new Set(bad_files)); + good_files = Array.from(new Set(good_files)); + viruses = Array.from(new Set(viruses)); + + // return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + + if (err) return (has_cb ? end_cb(err, [], bad_files, {}, []) : reject(new NodeClamError({bad_files}, err))); + return (has_cb ? end_cb(null, good_files, bad_files, {}, viruses) : resolve({good_files, bad_files, viruses, errors: null})); + }; + + // Use this method when scanning using local binaries + const local_scan = async () => { + // Get array of escaped file names + const items = files.map(file => file.replace(/ /g,'\\ ')); + + // Build the actual command to run + const command = `${self.settings[self.scanner].path} ${self._build_clam_args(items).join(' ')}`; + if (self.settings.debug_mode) + if (self.settings.debug_mode) console.log(`${self.debug_label}: Configured clam command: ${command}}`); + + // Execute the clam binary with the proper flags + execFile(command, (err, stdout, stderr) => { + if (self.settings.debug_mode) console.log(`${this.debug_label}: stdout:`, stdout); + + if (err) return parse_stdout(err, stdout); + + if (stderr) { + if (self.settings.debug_mode) console.log(`${this.debug_label}: `, stderr); + + if (stderr.length > 0) { + bad_files = stderr.split(os.EOL).map(err_line => { + const match = err_line.match(/^ERROR: Can't access file (.*)+$/); + if (match !== null && match.length > 1 && typeof match[1] === 'string') return match[1]; + return ''; + }); + + bad_files = bad_files.filter(v => !!v); + } + } + return parse_stdout(null, stdout); + }); + }; + + // This is the function that actually scans the files + const do_scan = async files => { + const num_files = files.length; + + if (self.settings.debug_mode) console.log(`${this.debug_label}: Scanning a list of ${num_files} passed files.`); + + // Slower but more verbose/informative way... + if (file_cb && typeof file_cb === 'function') { + // Scan files in parallel chunks of 10 + const chunk_size = 10; + let results = []; + while (files.length > 0) { + let chunk = []; + if (files.length > chunk_size) { + chunk = files.splice(0, chunk_size); + } else { + chunk = files.splice(0); + } + + // Scan 10 files then move to the next set... + const chunk_results = await Promise.all(chunk.map(file => this.is_infected(file).catch(e => e))); + + // Re-map results back to their filenames + const chunk_results_mapped = chunk_results.map((v,i) => [chunk[i], v]); + + // Trigger file-callback for each file that was just scanned + chunk_results_mapped.forEach(v => file_cb(null, v[0], v[1])); + + // Add mapped chunk results to overall scan results array + results = results.concat(chunk_results_mapped); + } + + // Build out the good and bad files arrays + results.forEach(v => { + if (v[1] === true) bad_files.push(v[0]); + else if (v[1] === false) good_files.push(v[0]); + else if (v[1] instanceof Error) { + errors[v[0]] = v[1]; + } + }); + + // Make sure the number of results matches the original number of files to be scanned + if (num_files !== results.length) { + const err_msg = 'The number of results did not match the number of files to scan!'; + return (has_cb ? end_cb(new NodeClamError(err_msg), good_files, bad_files, {}, []) : reject(new NodeClamError({good_files, bad_files}, err_msg))); + } + + // Make sure the list of bad and good files is unique...(just for good measure) + bad_files = Array.from(new Set(bad_files)); + good_files = Array.from(new Set(good_files)); + + if (self.settings.debug_mode) { + console.log(`${self.debug_label}: Scan Complete!`); + console.log(`${self.debug_label}: Num Bad Files: `, bad_files.length); + console.log(`${self.debug_label}: Num Good Files: `, good_files.length); + } + + return (has_cb ? end_cb(null, good_files, bad_files, {}, []) : resolve({good_files, bad_files, errors: null, viruses: []})); + } + + // The quicker but less-talkative way + else { + let all_files = []; + + // This is where we scan every file/path in the `all_files` array once it's been fully populated + const finish_scan = async () => { + // Make sure there are no dupes, falsy values, or non-strings... just because we can + all_files = Array.from(new Set(all_files.filter(v => !!v))).filter(v => typeof v === 'string'); + + // If file list is empty, return error + if (all_files.length <= 0) { + const err = new NodeClamError('No valid files provided to scan!'); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + + // If scanning via sockets, use that method, otherwise use `local_scan` + if (self.settings.clamdscan.socket || self.settings.clamdscan.port) { + const chunk_size = 10; + let results = []; + while (all_files.length > 0) { + let chunk = []; + if (all_files.length > chunk_size) { + chunk = all_files.splice(0, chunk_size); + } else { + chunk = all_files.splice(0); + } + + // Scan 10 files then move to the next set... + const chunk_results = await Promise.all(chunk.map(file => self.is_infected(file).catch(e => e))); + + // Re-map results back to their filenames + const chunk_results_mapped = chunk_results.map((v,i) => [chunk[i], v]); + //const chunk_results_mapped = chunk_results; + + // Add mapped chunk results to overall scan results array + results = results.concat(chunk_results_mapped); + } + + // Build out the good and bad files arrays + results.forEach(v => { + if (v[1] instanceof Error) errors[v[0]] = v[1]; + else if (typeof v[1] === 'object' && 'is_infected' in v[1] && v[1].is_infected === true) { + bad_files.push(v[1].file); + if ('viruses' in v[1] && Array.isArray(v[1].viruses) && v[1].viruses.length > 0) { + viruses = viruses.concat(v[1].viruses); + } + } + else if (typeof v[1] === 'object' && 'is_infected' in v[1] && v[1].is_infected === false) { + good_files.push(v[1].file); + } + }); + + // Make sure the list of bad and good files is unique...(just for good measure) + bad_files = Array.from(new Set(bad_files)); + good_files = Array.from(new Set(good_files)); + viruses = Array.from(new Set(viruses)); + + if (self.settings.debug_mode) { + console.log(`${self.debug_label}: Scan Complete!`); + console.log(`${self.debug_label}: Num Bad Files: `, bad_files.length); + console.log(`${self.debug_label}: Num Good Files: `, good_files.length); + console.log(`${self.debug_label}: Num Viruses: `, viruses.length); + } + + return (has_cb ? end_cb(null, good_files, bad_files, errors, viruses) : resolve({errors, good_files, bad_files, viruses})); + } else { + return local_scan(); + } + }; + + // If clamdscan is the preferred binary but we don't want to scan recursively + // then we need to convert all path entries to a list of files found in the + // first layer of that path + if (this.scan_recursively === false && this.scanner === 'clamdscan') { + const chunk_size = 10; + while (files.length > 0) { + let chunk = []; + if (files.length > chunk_size) { + chunk = files.splice(0, chunk_size); + } else { + chunk = files.splice(0); + } + + // Scan 10 files then move to the next set... + const chunk_results = await Promise.all(chunk.map(file => fs_stat(file).catch(e => e))); + + // Add each file to `all_files` array + // chunk_results.forEach(async (v,i) => { + for (let i in chunk_results) { + const v = chunk_results[i]; + // If the result is an error, add it to the error + // object and skip adding this file to the `all_files` array + if (v instanceof Error) { + errors[chunk[i]] = v; + } else if (v.isFile()) { + all_files.push(chunk[i]); + } else if (v.isDirectory()) { + const rgx = new RegExp(`^(?!${v})(.+)$`); + try { + const contents = (await fs_readdir(chunk[i], {withFileTypes: true})).filter(x => x.isFile()).map(x => x.name.replace(rgx, `${v}/${x.name}`)); + all_files = all_files.concat(contents); + } catch (e) { + errors[chunk[i]] = e; + } + } + } + + // Scan the files in the all_files array + return finish_scan(); + } + } else { + // Just scan all the files + all_files = files; + + // Scan the files in the all_files array + return finish_scan(); + } + } + }; + + // If string is provided in files param, forgive them... create a single element array + if (typeof files === 'string' && files.trim().length > 0) { + files = files.trim().split(',').map(v => v.trim()); + } + + // If the files array is actually an array, do some additional validation + if (Array.isArray(files)) { + // Keep track of the original number of files specified + orig_num_files = files.length; + + // Remove any empty or non-string elements + files = files.filter(v => !!v).filter(v => typeof v === 'string'); + + // If any items specified were not valid strings, fail... + if (files.length < orig_num_files) { + const err = new NodeClamError({num_files: files.length, orig_num_files}, 'You\'ve specified at least one invalid item to the files list (first parameter) of the `scan_files` method.'); + // console.log("Files: ", files); + // console.log("Num Files: ", files.length); + // console.log("Original Num Files: ", orig_num_files); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + } + + // Do some parameter validation + if (!Array.isArray(files) || files.length <= 0) { + // Before failing completely, check if there is a file list specified + if (!('file_list' in this.settings) || !this.settings.file_list) { + const err = new NodeClamError({files, settings: this.settings}, 'No files provided to scan and no file list provided!'); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + + // If the file list is specified, read it in and scan listed files... + try { + const data = (await fs_readfile(this.settings.file_list)).toString().split(os.EOL); + return do_scan(data); + } catch (e) { + const err = new NodeClamError({err: e, file_list: this.settings.file_list}, `No files provided and file list was provided but could not be found! ${e}`); + return (has_cb ? end_cb(err, [], [], {}, []) : reject(err)); + } + } else { + return do_scan(files); + } + }); + } + + // **************************************************************************** + // Scans an entire directory. Provides 3 params to end callback: Error, path + // scanned, and whether its infected or not. To scan multiple directories, pass + // them as an array to the `scan_files` method. + // ----- + // NOTE: While possible, it is NOT advisable to use the file_cb parameter when + // using the clamscan binary. Doing so with clamdscan is okay, however. This + // method also allows for non-recursive scanning with the clamdscan binary. + // ----- + // @param String path The directory to scan files of + // @param Function end_cb (optional) What to do when all files have been scanned + // @param Function file_cb (optional) What to do after each file has been scanned + // @return Promise + // **************************************************************************** + scan_dir(path='', end_cb=null, file_cb=null) { + const self = this; + let has_cb = false; + + // Verify second param, if supplied, is a function + if (end_cb && typeof end_cb !== 'function') { + throw new NodeClamError('Invalid end-scan callback provided to `scan_dir`. Second paramter, if provided, must be a function!'); + } else if (end_cb && typeof end_cb === 'function') { + has_cb = true; + } + + // At this point for the hybrid Promise/CB API to work, everything needs to be wrapped + // in a Promise that will be returned + return new Promise(async (resolve, reject) => { + // Verify `path` provided is a string + if (typeof path !== 'string' || (typeof path === 'string' && path.trim() === '')) { + const err = new NodeClamError({path}, 'Invalid path provided! Path must be a string!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + + // Normalize and then trim trailing slash + path = node_path.normalize(path).replace(/\/$/, ''); + + // Make sure path exists... + try { + await fs_access(path, fs.constants.R_OK); + } catch (e) { + const err = new NodeClamError({path, err:e}, 'Invalid path specified to scan!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + + // Execute the clam binary with the proper flags + const local_scan = () => { + execFile(self.settings[self.scanner].path, self._build_clam_args(path), (err, stdout, stderr) => { + const {is_infected, viruses} = self._process_result(stdout, path); + + if (err) { + // Error code 1 means viruses were found... + if (Object.prototype.hasOwnProperty.call(err, 'code') && err.code === 1) { + return (has_cb ? end_cb(null, [], [path], viruses) : resolve({path, is_infected, bad_files: [path], good_files: [], viruses})); + } else { + const error = new NodeClamError({path, err}, 'There was an error scanning the path or processing the result.'); + return (has_cb ? end_cb(error, [], [], []) : reject(error)); + } + } + + if (stderr) { + console.error(`${self.debug_label} error: `, stderr); + return (has_cb ? end_cb(null, [], [], []) : resolve({stderr, path, is_infected, good_files: [], bad_files: [], viruses})); + } + + const good_files = (is_infected ? [] : [path]); + const bad_files = (is_infected ? [path] : []); + return (has_cb ? end_cb(null, good_files, bad_files, viruses) : resolve({path, is_infected, good_files, bad_files, viruses})); + }); + }; + + // Get all files recursively using `scan_files` + if (this.settings.scan_recursively === true && (typeof file_cb === 'function' || !has_cb)) { + try { + const {stdout, stderr} = await cp_execfile('find', [path]); + + if (stderr) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: `, stderr); + return (has_cb ? end_cb(null, [], []) : resolve({stderr, path, is_infected: null, good_files: [], bad_files: [], viruses: []})); + } + + const files = stdout.trim().split(os.EOL).map(path => path.replace(/ /g,'\\ ').trim()); + return this.scan_files(files, end_cb, file_cb); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'There was an issue scanning the path specified!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + // Clamdscan always does recursive, so, here's a way to avoid that if you want (will call `scan_files` method) + else if (this.settings.scan_recursively === false && this.scanner === 'clamdscan') { + try { + const all_files = (await fs_readdir(path)).filter(async file => (await fs_stat(file)).isFile()); + return this.scan_files(all_files, end_cb, file_cb); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'Could not read the file listing of the path provided.'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + + // If you don't care about individual file progress (which is very slow for clamscan but fine for clamdscan...) + // NOTE: This section WILL scan recursively + else if (typeof file_cb !== 'function' || !has_cb) { + // Scan locally via socket (either TCP or Unix socket) + // This is much simpler/faster process--potentially even more with MULTISCAN enabled) + if (this.settings.clamdscan.socket || (this.settings.clamdscan.port && this._is_localhost())) { + try { + const client = await this._init_socket(); + if (this.settings.debug_mode) console.log(`${this.debug_label}: scanning path with local domain socket now.`); + + if (this.settings.clamdscan.multiscan === true) { + // Use Multiple threads (faster) + client.write(`MULTISCAN ${path}`); + } else { + // Use single or default # of threads (potentially slower) + client.write(`SCAN ${path}`); + } + + // Where to buffer string response (not a real "Buffer", per se...) + const chunks = []; + + // Read output of the ClamAV socket to see what it's saying and when + // it's done saying it (FIN) + client + // ClamAV is sending stuff to us + .on('data', chunk => { + chunks.push(chunk); + }) + // ClamAV is done sending stuff to us + .on('end', async () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Received response from remote clamd service.`); + const response = Buffer.concat(chunks); + + const result = this._process_result(response.toString(), path); + if (result instanceof Error) { + // Fallback to local if that's an option + if (this.settings.clamdscan.local_fallback === true) return local_scan(); + const err = new NodeClamError({path, err: result}, 'There was an issue scanning the path provided.'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + + const {is_infected, viruses} = result; + const good_files = (is_infected ? [] : [path]); + const bad_files = (is_infected ? [path] : []); + return (has_cb ? end_cb(null, good_files, bad_files, viruses) : resolve({path, is_infected, good_files, bad_files, viruses})); + }); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'There was an issue scanning the path provided.'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + + // Scan path recursively using remote host/port and TCP protocol (must stream every single file to it...) + // WARNING: This is going to be really slow + else if (this.settings.clamdscan.port && !this._is_localhost()) { + const results = []; + + try { + const {stdout, stderr} = await cp_execfile('find', [path]); + + if (stderr) { + if (this.settings.debug_mode) console.log(`${this.debug_label}: `, stderr); + return (has_cb ? end_cb(null, [], []) : resolve({stderr, path, is_infected, good_files: [], bad_files: [], viruses: []})); + } + + // Get the proper recursive list of files from the path + const files = stdout.split('\n').map(path => path.replace(/ /g,'\\ ')); + + // Send files to remote server in parallel chunks of 10 + const chunk_size = 10; + while (files.length > 0) { + let chunk = []; + if (files.length > chunk_size) { + chunk = files.splice(0, chunk_size); + } else { + chunk = files.splice(0); + } + + // Scan 10 files then move to the next set... + results.concat(await Promise.all(chunk.map(file => this.scan_stream(fs.createReadStream(file))))); + } + + // If even a single file is infected, the whole directory is infected + const is_infected = results.any(v => v === false); + const good_files = (is_infected ? [] : [path]); + const bad_files = (is_infected ? [path] : []); + return (has_cb ? end_cb(null, good_files, bad_files) : resolve({path, is_infected, good_files, bad_files, viruses: []})); + } catch (e) { + const err = new NodeClamError({path, err: e}, 'Invalid path provided! Path must be a string!'); + return (has_cb ? end_cb(err, [], []) : reject(err)); + } + } + + // Scan locally + else { + local_scan(); + } + } + }); + } + + // **************************************************************************** + // Scans a node Stream object + // ----- + // @param Stream stream The stream to scan + // @param Function callback (optional) What to do when the socket responds with results + // @return Promise + // **************************************************************************** + scan_stream(stream, cb) { + let has_cb = false; + + // Verify second param, if supplied, is a function + if (cb && typeof cb !== 'function') + throw new NodeClamError('Invalid cb provided to scan_stream. Second paramter must be a function!'); + + // Making things simpler + if (cb && typeof cb === 'function') has_cb = true; + + return new Promise(async (resolve, reject) => { + let finished = false; + + // Verify stream is passed to the first parameter + if (!this._is_readable_stream(stream)) { + const err = new NodeClamError({stream}, 'Invalid stream provided to scan.'); + return (has_cb ? cb(err, null) : reject(err)); + } else { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Provided stream is readable.`); + } + + // Verify that they have a valid socket or host/port config + if (!this.settings.clamdscan.socket && (!this.settings.clamdscan.port || !this.settings.clamdscan.host)) { + const err = new NodeClamError({clamdscan_settings: this.settings.clamdscan}, 'Invalid information provided to connect to clamav service. A unix socket or port (+ optional host) is required!'); + return (has_cb ? cb(err, null) : reject(err)); + } + + // Get a socket client + try { + // Get an instance of our stream tranform that coddles + // the chunks from the incoming stream to what ClamAV wants + const transform = new NodeClamTransform({}, this.settings.debug_mode); + + // Get a socket + const socket = await this._init_socket(); + + // Pipe the stream through our transform and into the ClamAV socket + stream.pipe(transform).pipe(socket); + + // Setup the listeners for the stream + stream + // The stream has dried up + .on('end', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: The input stream has dried up.`); + finished = true; + stream.destroy(); + }) + // There was an error with the stream (ex. uploader closed browser) + .on('error', err => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: There was an error with the input stream (maybe uploader closed browser?).`, err); + return (has_cb ? cb(err, null) : reject(err)); + }); + + + + // Where to buffer string response (not a real "Buffer", per se...) + const chunks = []; + + // Read output of the ClamAV socket to see what it's saying and when + // it's done saying it (FIN) + socket + // ClamAV is sending stuff to us + .on('data', chunk => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Received output from ClamAV Socket.`); + if (!stream.isPaused()) stream.pause(); + chunks.push(chunk); + }) + + .on('close', hadError => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: ClamAV socket has been closed!`, hadError); + }) + + .on('error', err => { + console.error(`${this.debug_label}: Error emitted from ClamAV socket: `, err); + return (has_cb ? cb(err, null) : reject(err)); + }) + + // ClamAV is done sending stuff to us + .on('end', () => { + if (this.settings.debug_mode) console.log(`${this.debug_label}: ClamAV is done scanning.`); + const response = Buffer.concat(chunks); + if (!finished) { + const err = new NodeClamError('Scan aborted. Reply from server: ' + response.toString('utf8')); + return (has_cb ? cb(err, null) : reject(err)); + } else { + if (this.settings.debug_mode) console.log(`${this.debug_label}: Raw Response: ${response.toString('utf8')}`); + const result = this._process_result(response.toString('utf8'), null); + return (has_cb ? cb(null, result) : resolve(result)); + } + }); + } catch (err) { + return (has_cb ? cb(err, null) : reject(err)); + } + }); + } +} diff --git a/benchmarks/pairs/vfc-js/diskusage-ng-0-2-6/fixed.js b/benchmarks/pairs/vfc-js/diskusage-ng-0-2-6/fixed.js new file mode 100644 index 0000000..0df2d43 --- /dev/null +++ b/benchmarks/pairs/vfc-js/diskusage-ng-0-2-6/fixed.js @@ -0,0 +1,26 @@ +// Provenance: iximiuz/node-diskusage-ng (fixed). +// repo: iximiuz/node-diskusage-ng +// commit: 48e7e093486b528f0c81ec699573e0e4a431b8d3 +// parent: 53c505506553c83861d1bd67823d6a382d3a5a43 +// commit_url: https://github.com/iximiuz/node-diskusage-ng/commit/48e7e093486b528f0c81ec699573e0e4a431b8d3 +// cve: CVE-2020-7631 +// license: MIT +// function: diskusage +// relpath: lib/posix.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 6 + +function diskusage(path, cb) { + execFile('df', ['-k', path], function(err, stdout) { + if (err) { + return cb(err); + } + + try { + cb(null, parse(stdout)); + } catch (e) { + cb(e); + } + }); +} diff --git a/benchmarks/pairs/vfc-js/diskusage-ng-0-2-6/vuln.js b/benchmarks/pairs/vfc-js/diskusage-ng-0-2-6/vuln.js new file mode 100644 index 0000000..e209456 --- /dev/null +++ b/benchmarks/pairs/vfc-js/diskusage-ng-0-2-6/vuln.js @@ -0,0 +1,30 @@ +// Provenance: iximiuz/node-diskusage-ng (vuln). +// repo: iximiuz/node-diskusage-ng +// commit: 53c505506553c83861d1bd67823d6a382d3a5a43 +// parent: 53c505506553c83861d1bd67823d6a382d3a5a43 +// commit_url: https://github.com/iximiuz/node-diskusage-ng/commit/48e7e093486b528f0c81ec699573e0e4a431b8d3 +// cve: CVE-2020-7631 +// license: MIT +// function: diskusage +// relpath: lib/posix.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 6 + +function diskusage(path, cb) { + if (path.indexOf('"') !== -1) { + return cb(new Error('Paths with double quotes are not supported yet')); + } + + exec('df -k "' + path + '"', function(err, stdout) { + if (err) { + return cb(err); + } + + try { + cb(null, parse(stdout)); + } catch (e) { + cb(e); + } + }); +} diff --git a/benchmarks/pairs/vfc-js/dns-sync-0-1-0/fixed.js b/benchmarks/pairs/vfc-js/dns-sync-0-1-0/fixed.js new file mode 100644 index 0000000..d036e68 --- /dev/null +++ b/benchmarks/pairs/vfc-js/dns-sync-0-1-0/fixed.js @@ -0,0 +1,38 @@ +// Provenance: skoranga/node-dns-sync (fixed). +// repo: skoranga/node-dns-sync +// commit: d9abaae384b198db1095735ad9c1c73d7b890a0d +// parent: b748fe77a87749d95876028eda8e759109be4c73 +// commit_url: https://github.com/skoranga/node-dns-sync/commit/d9abaae384b198db1095735ad9c1c73d7b890a0d +// cve: CVE-2014-9682 +// license: MIT +// function: resolve +// relpath: lib/dns-sync.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 19 + +module.exports = { + resolve: function resolve(hostname) { + var output, + nodeBinary = process.execPath; + + if (!isValidHostName(hostname)) { + console.error('Invalid hostname:', hostname); + return null; + } + + var scriptPath = path.join(__dirname, "../scripts/dns-lookup-script"), + response, + cmd = util.format('"%s" "%s" %s', nodeBinary, scriptPath, hostname); + + response = shell.exec(cmd, {silent: true}); + if (response && response.code === 0) { + output = response.output; + if (output && net.isIP(output)) { + return output; + } + } + debug('hostname', "fail to resolve hostname " + hostname); + return null; + } +}; diff --git a/benchmarks/pairs/vfc-js/dns-sync-0-1-0/vuln.js b/benchmarks/pairs/vfc-js/dns-sync-0-1-0/vuln.js new file mode 100644 index 0000000..0780599 --- /dev/null +++ b/benchmarks/pairs/vfc-js/dns-sync-0-1-0/vuln.js @@ -0,0 +1,32 @@ +// Provenance: skoranga/node-dns-sync (vuln). +// repo: skoranga/node-dns-sync +// commit: b748fe77a87749d95876028eda8e759109be4c73 +// parent: b748fe77a87749d95876028eda8e759109be4c73 +// commit_url: https://github.com/skoranga/node-dns-sync/commit/d9abaae384b198db1095735ad9c1c73d7b890a0d +// cve: CVE-2014-9682 +// license: MIT +// function: resolve +// relpath: lib/dns-sync.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 13 + +module.exports = { + resolve: function resolve(hostname) { + var output, + nodeBinary = process.execPath, + scriptPath = path.join(__dirname, "../scripts/dns-lookup-script"), + response, + cmd = util.format('"%s" "%s" %s', nodeBinary, scriptPath, hostname); + + response = shell.exec(cmd, {silent: true}); + if (response && response.code === 0) { + output = response.output; + if (output && net.isIP(output)) { + return output; + } + } + debug('hostname', "fail to resolve hostname " + hostname); + return null; + } +}; diff --git a/benchmarks/pairs/vfc-js/glance-3-0-0/fixed.js b/benchmarks/pairs/vfc-js/glance-3-0-0/fixed.js new file mode 100644 index 0000000..80b38c7 --- /dev/null +++ b/benchmarks/pairs/vfc-js/glance-3-0-0/fixed.js @@ -0,0 +1,92 @@ +// Provenance: jarofghosts/glance (fixed). +// repo: jarofghosts/glance +// commit: 8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19 +// parent: 7e69a5539236386e747fb240ae87ea65ed64bb2c +// commit_url: https://github.com/jarofghosts/glance/commit/8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19 +// cve: CVE-2018-3715 +// license: MIT +// function: Glance$serveRequest +// relpath: index.js +// provenance: human +// mechanism: path_join_user_input +// upstream_start: 60 + +Glance.prototype.serveRequest = function Glance$serveRequest (req, res) { + var request = {} + var self = this + + request.fullPath = path.join( + self.dir, + decodeURIComponent(parse(req.url).pathname) + ) + + request.ip = req.socket.remoteAddress + request.method = req.method.toLowerCase() + request.response = res + + // prevent traversing directories that are parents of the root + if (request.fullPath.slice(0, self.dir.length) !== self.dir) { + return self.emit('error', 403, request, res) + } + + if (request.method !== 'get') { + return self.emit('error', 405, request, res) + } + + if (self.nodot && /^\./.test(path.basename(request.fullPath))) { + return self.emit('error', 404, request, res) + } + + fs.stat(request.fullPath, statFile) + + function statFile (err, stat) { + if (err) { + return self.emit('error', 404, request, res) + } + + if (!stat.isDirectory()) { + self.emit('read', request) + + return filed(request.fullPath).pipe(res) + } + + if (self.hideindex) { + return self.emit('error', 403, request, res) + } + + if (!self.indices || !self.indices.length) { + return listFiles() + } + + var indices = self.indices.slice() + + findIndex(indices.shift()) + + function findIndex (indexTest) { + fileExists(path.join(request.fullPath, indexTest), check) + + function check (hasIndex) { + if (hasIndex) { + req.url = req.url + '/' + indexTest + + return self.serveRequest(req, res) + } + + if (!indices.length) { + return listFiles() + } + + findIndex(indices.shift()) + } + } + + function listFiles () { + var listPath = request.fullPath.replace(/\/$/, '') + + res.writeHead(200, RESPONSE_HEADERS) + htmlls(listPath, {hideDot: self.nodot}).pipe(res) + + return self.emit('read', request) + } + } +} diff --git a/benchmarks/pairs/vfc-js/glance-3-0-0/vuln.js b/benchmarks/pairs/vfc-js/glance-3-0-0/vuln.js new file mode 100644 index 0000000..519d032 --- /dev/null +++ b/benchmarks/pairs/vfc-js/glance-3-0-0/vuln.js @@ -0,0 +1,87 @@ +// Provenance: jarofghosts/glance (vuln). +// repo: jarofghosts/glance +// commit: 7e69a5539236386e747fb240ae87ea65ed64bb2c +// parent: 7e69a5539236386e747fb240ae87ea65ed64bb2c +// commit_url: https://github.com/jarofghosts/glance/commit/8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19 +// cve: CVE-2018-3715 +// license: MIT +// function: Glance$serveRequest +// relpath: index.js +// provenance: human +// mechanism: path_join_user_input +// upstream_start: 60 + +Glance.prototype.serveRequest = function Glance$serveRequest (req, res) { + var request = {} + var self = this + + request.fullPath = path.join( + self.dir, + decodeURIComponent(parse(req.url).pathname) + ) + + request.ip = req.socket.remoteAddress + request.method = req.method.toLowerCase() + request.response = res + + if (request.method !== 'get') { + return self.emit('error', 405, request, res) + } + + if (self.nodot && /^\./.test(path.basename(request.fullPath))) { + return self.emit('error', 404, request, res) + } + + fs.stat(request.fullPath, statFile) + + function statFile (err, stat) { + if (err) { + return self.emit('error', 404, request, res) + } + + if (!stat.isDirectory()) { + self.emit('read', request) + + return filed(request.fullPath).pipe(res) + } + + if (self.hideindex) { + return self.emit('error', 403, request, res) + } + + if (!self.indices || !self.indices.length) { + return listFiles() + } + + var indices = self.indices.slice() + + findIndex(indices.shift()) + + function findIndex (indexTest) { + fileExists(path.join(request.fullPath, indexTest), check) + + function check (hasIndex) { + if (hasIndex) { + req.url = req.url + '/' + indexTest + + return self.serveRequest(req, res) + } + + if (!indices.length) { + return listFiles() + } + + findIndex(indices.shift()) + } + } + + function listFiles () { + var listPath = request.fullPath.replace(/\/$/, '') + + res.writeHead(200, RESPONSE_HEADERS) + htmlls(listPath, {hideDot: self.nodot}).pipe(res) + + return self.emit('read', request) + } + } +} diff --git a/benchmarks/pairs/vfc-js/hot-formula-parser-3-0-0/fixed.js b/benchmarks/pairs/vfc-js/hot-formula-parser-3-0-0/fixed.js new file mode 100644 index 0000000..851190d --- /dev/null +++ b/benchmarks/pairs/vfc-js/hot-formula-parser-3-0-0/fixed.js @@ -0,0 +1,867 @@ +// Provenance: handsontable/formula-parser (fixed). +// repo: handsontable/formula-parser +// commit: 396b089738d4bf30eb570a4fe6a188affa95cd5e +// parent: 944136096f44ff4ffd4ba0bcef30970ca4702fce +// commit_url: https://github.com/handsontable/formula-parser/commit/396b089738d4bf30eb570a4fe6a188affa95cd5e +// cve: CVE-2020-6836 +// license: MIT +// function: performAction +// relpath: src/grammar-parser/grammar-parser.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 74 + +var grammarParser = (function(){ +var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,5],$V1=[1,8],$V2=[1,6],$V3=[1,7],$V4=[1,9],$V5=[1,14],$V6=[1,15],$V7=[1,16],$V8=[1,12],$V9=[1,13],$Va=[1,17],$Vb=[1,19],$Vc=[1,20],$Vd=[1,21],$Ve=[1,22],$Vf=[1,23],$Vg=[1,24],$Vh=[1,25],$Vi=[1,26],$Vj=[1,27],$Vk=[1,28],$Vl=[5,9,10,11,13,14,15,16,17,18,19,20,29,30],$Vm=[5,9,10,11,13,14,15,16,17,18,19,20,29,30,32],$Vn=[5,9,10,11,13,14,15,16,17,18,19,20,29,30,34],$Vo=[5,10,11,13,14,15,16,17,29,30],$Vp=[5,10,13,14,15,16,29,30],$Vq=[5,10,11,13,14,15,16,17,18,19,29,30],$Vr=[13,29,30]; +var parser = {trace: function trace () { }, +yy: {}, +symbols_: {"error":2,"expressions":3,"expression":4,"EOF":5,"variableSequence":6,"number":7,"STRING":8,"&":9,"=":10,"+":11,"(":12,")":13,"<":14,">":15,"NOT":16,"-":17,"*":18,"/":19,"^":20,"FUNCTION":21,"expseq":22,"cell":23,"ABSOLUTE_CELL":24,"RELATIVE_CELL":25,"MIXED_CELL":26,":":27,"ARRAY":28,";":29,",":30,"VARIABLE":31,"DECIMAL":32,"NUMBER":33,"%":34,"ERROR":35,"$accept":0,"$end":1}, +terminals_: {5:"EOF",8:"STRING",9:"&",10:"=",11:"+",12:"(",13:")",14:"<",15:">",16:"NOT",17:"-",18:"*",19:"/",20:"^",21:"FUNCTION",24:"ABSOLUTE_CELL",25:"RELATIVE_CELL",26:"MIXED_CELL",27:":",28:"ARRAY",29:";",30:",",31:"VARIABLE",32:"DECIMAL",33:"NUMBER",34:"%",35:"ERROR"}, +productions_: [0,[3,2],[4,1],[4,1],[4,1],[4,3],[4,3],[4,3],[4,3],[4,4],[4,4],[4,4],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,2],[4,2],[4,3],[4,4],[4,1],[4,1],[4,2],[23,1],[23,1],[23,1],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[22,1],[22,1],[22,3],[22,3],[6,1],[6,3],[7,1],[7,3],[7,2],[2,1]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1: + + return $$[$0-1]; + +break; +case 2: + + this.$ = yy.callVariable($$[$0][0]); + +break; +case 3: + + this.$ = yy.toNumber($$[$0]); + +break; +case 4: + + this.$ = yy.trimEdges($$[$0]); + +break; +case 5: + + this.$ = yy.evaluateByOperator('&', [$$[$0-2], $$[$0]]); + +break; +case 6: + + this.$ = yy.evaluateByOperator('=', [$$[$0-2], $$[$0]]); + +break; +case 7: + + this.$ = yy.evaluateByOperator('+', [$$[$0-2], $$[$0]]); + +break; +case 8: + + this.$ = $$[$0-1]; + +break; +case 9: + + this.$ = yy.evaluateByOperator('<=', [$$[$0-3], $$[$0]]); + +break; +case 10: + + this.$ = yy.evaluateByOperator('>=', [$$[$0-3], $$[$0]]); + +break; +case 11: + + this.$ = yy.evaluateByOperator('<>', [$$[$0-3], $$[$0]]); + +break; +case 12: + + this.$ = yy.evaluateByOperator('NOT', [$$[$0-2], $$[$0]]); + +break; +case 13: + + this.$ = yy.evaluateByOperator('>', [$$[$0-2], $$[$0]]); + +break; +case 14: + + this.$ = yy.evaluateByOperator('<', [$$[$0-2], $$[$0]]); + +break; +case 15: + + this.$ = yy.evaluateByOperator('-', [$$[$0-2], $$[$0]]); + +break; +case 16: + + this.$ = yy.evaluateByOperator('*', [$$[$0-2], $$[$0]]); + +break; +case 17: + + this.$ = yy.evaluateByOperator('/', [$$[$0-2], $$[$0]]); + +break; +case 18: + + this.$ = yy.evaluateByOperator('^', [$$[$0-2], $$[$0]]); + +break; +case 19: + + var n1 = yy.invertNumber($$[$0]); + + this.$ = n1; + + if (isNaN(this.$)) { + this.$ = 0; + } + +break; +case 20: + + var n1 = yy.toNumber($$[$0]); + + this.$ = n1; + + if (isNaN(this.$)) { + this.$ = 0; + } + +break; +case 21: + + this.$ = yy.callFunction($$[$0-2]); + +break; +case 22: + + this.$ = yy.callFunction($$[$0-3], $$[$0-1]); + +break; +case 26: case 27: case 28: + + this.$ = yy.cellValue($$[$0]); + +break; +case 29: case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: + + this.$ = yy.rangeValue($$[$0-2], $$[$0]); + +break; +case 38: case 42: + + this.$ = [$$[$0]]; + +break; +case 39: + + this.$ = yy.trimEdges(yytext).split(','); + +break; +case 40: case 41: + + $$[$0-2].push($$[$0]); + this.$ = $$[$0-2]; + +break; +case 43: + + this.$ = (Array.isArray($$[$0-2]) ? $$[$0-2] : [$$[$0-2]]); + this.$.push($$[$0]); + +break; +case 44: + + this.$ = $$[$0]; + +break; +case 45: + + this.$ = ($$[$0-2] + '.' + $$[$0]) * 1; + +break; +case 46: + + this.$ = $$[$0-1] * 0.01; + +break; +case 47: + + this.$ = yy.throwError($$[$0]); + +break; +} +}, +table: [{2:11,3:1,4:2,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{1:[3]},{5:[1,18],9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk},o($Vl,[2,2],{32:[1,29]}),o($Vl,[2,3],{34:[1,30]}),o($Vl,[2,4]),{2:11,4:31,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:32,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:33,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{12:[1,34]},o($Vl,[2,23]),o($Vl,[2,24],{2:35,35:$Va}),o($Vm,[2,42]),o($Vn,[2,44],{32:[1,36]}),o($Vl,[2,26],{27:[1,37]}),o($Vl,[2,27],{27:[1,38]}),o($Vl,[2,28],{27:[1,39]}),o([5,9,10,11,13,14,15,16,17,18,19,20,29,30,35],[2,47]),{1:[2,1]},{2:11,4:40,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:41,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:42,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:45,6:3,7:4,8:$V0,10:[1,43],11:$V1,12:$V2,15:[1,44],17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:47,6:3,7:4,8:$V0,10:[1,46],11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:48,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:49,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:50,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:51,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:52,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{31:[1,53]},o($Vn,[2,46]),{9:$Vb,10:$Vc,11:$Vd,13:[1,54],14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk},o($Vo,[2,19],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),o($Vo,[2,20],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),{2:11,4:57,6:3,7:4,8:$V0,11:$V1,12:$V2,13:[1,55],17:$V3,21:$V4,22:56,23:10,24:$V5,25:$V6,26:$V7,28:[1,58],31:$V8,33:$V9,35:$Va},o($Vl,[2,25]),{33:[1,59]},{24:[1,60],25:[1,61],26:[1,62]},{24:[1,63],25:[1,64],26:[1,65]},{24:[1,66],25:[1,67],26:[1,68]},o($Vl,[2,5]),o([5,10,13,29,30],[2,6],{9:$Vb,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vo,[2,7],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),{2:11,4:69,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:70,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},o($Vp,[2,14],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),{2:11,4:71,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},o($Vp,[2,13],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o([5,10,13,16,29,30],[2,12],{9:$Vb,11:$Vd,14:$Ve,15:$Vf,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vo,[2,15],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),o($Vq,[2,16],{9:$Vb,20:$Vk}),o($Vq,[2,17],{9:$Vb,20:$Vk}),o([5,10,11,13,14,15,16,17,18,19,20,29,30],[2,18],{9:$Vb}),o($Vm,[2,43]),o($Vl,[2,8]),o($Vl,[2,21]),{13:[1,72],29:[1,73],30:[1,74]},o($Vr,[2,38],{9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vr,[2,39]),o($Vn,[2,45]),o($Vl,[2,29]),o($Vl,[2,30]),o($Vl,[2,31]),o($Vl,[2,32]),o($Vl,[2,33]),o($Vl,[2,34]),o($Vl,[2,35]),o($Vl,[2,36]),o($Vl,[2,37]),o($Vp,[2,9],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vp,[2,11],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vp,[2,10],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vl,[2,22]),{2:11,4:75,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:76,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},o($Vr,[2,40],{9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vr,[2,41],{9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk})], +defaultActions: {18:[2,1]}, +parseError: function parseError (str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + var error = new Error(str); + error.hash = hash; + throw error; + } +}, +parse: function parse (input) { + var self = this, + stack = [0], + tstack = [], // token stack + vstack = [null], // semantic value stack + lstack = [], // location stack + table = this.table, + yytext = '', + yylineno = 0, + yyleng = 0, + recovering = 0, + TERROR = 2, + EOF = 1; + + var args = lstack.slice.call(arguments, 1); + + //this.reductionCount = this.shiftCount = 0; + + var lexer = Object.create(this.lexer); + var sharedState = { yy: {} }; + // copy state + for (var k in this.yy) { + if (Object.prototype.hasOwnProperty.call(this.yy, k)) { + sharedState.yy[k] = this.yy[k]; + } + } + + lexer.setInput(input, sharedState.yy); + sharedState.yy.lexer = lexer; + sharedState.yy.parser = this; + if (typeof lexer.yylloc == 'undefined') { + lexer.yylloc = {}; + } + var yyloc = lexer.yylloc; + lstack.push(yyloc); + + var ranges = lexer.options && lexer.options.ranges; + + if (typeof sharedState.yy.parseError === 'function') { + this.parseError = sharedState.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + + function popStack (n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + +_token_stack: + var lex = function () { + var token; + token = lexer.lex() || EOF; + // if token isn't its numeric value, convert + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + } + + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + // retreive state number from top of stack + state = stack[stack.length - 1]; + + // use default actions if available + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + // read action for current state and first input + action = table[state] && table[state][symbol]; + } + +_handle_error: + // handle parse error + if (typeof action === 'undefined' || !action.length || !action[0]) { + var error_rule_depth; + var errStr = ''; + + // Return the rule stack depth where the nearest error rule can be found. + // Return FALSE when no error recovery rule was found. + function locateNearestErrorRecoveryRule(state) { + var stack_probe = stack.length - 1; + var depth = 0; + + // try to recover from error + for(;;) { + // check for error recovery rule in this state + if ((TERROR.toString()) in table[state]) { + return depth; + } + if (state === 0 || stack_probe < 2) { + return false; // No suitable error recovery rule available. + } + stack_probe -= 2; // popStack(1): [symbol, action] + state = stack[stack_probe]; + ++depth; + } + } + + if (!recovering) { + // first see if there's any chance at hitting an error recovery rule: + error_rule_depth = locateNearestErrorRecoveryRule(state); + + // Report error + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push("'"+this.terminals_[p]+"'"); + } + } + if (lexer.showPosition) { + errStr = 'Parse error on line '+(yylineno+1)+":\n"+lexer.showPosition()+"\nExpecting "+expected.join(', ') + ", got '" + (this.terminals_[symbol] || symbol)+ "'"; + } else { + errStr = 'Parse error on line '+(yylineno+1)+": Unexpected " + + (symbol == EOF ? "end of input" : + ("'"+(this.terminals_[symbol] || symbol)+"'")); + } + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected, + recoverable: (error_rule_depth !== false) + }); + } else if (preErrorSymbol !== EOF) { + error_rule_depth = locateNearestErrorRecoveryRule(state); + } + + // just recovered from another error + if (recovering == 3) { + if (symbol === EOF || preErrorSymbol === EOF) { + throw new Error(errStr || 'Parsing halted while starting to recover from another error.'); + } + + // discard current lookahead and grab another + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + symbol = lex(); + } + + // try to recover from error + if (error_rule_depth === false) { + throw new Error(errStr || 'Parsing halted. No suitable error recovery rule available.'); + } + popStack(error_rule_depth); + + preErrorSymbol = (symbol == TERROR ? null : symbol); // save the lookahead token + symbol = TERROR; // insert generic error symbol as new lookahead + state = stack[stack.length-1]; + action = table[state] && table[state][TERROR]; + recovering = 3; // allow 3 real symbols to be shifted before reporting a new error + } + + // this shouldn't happen, unless resolve defaults are off + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: '+state+', token: '+symbol); + } + + switch (action[0]) { + case 1: // shift + //this.shiftCount++; + + stack.push(symbol); + vstack.push(lexer.yytext); + lstack.push(lexer.yylloc); + stack.push(action[1]); // push state + symbol = null; + if (!preErrorSymbol) { // normal execution/no error + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + // error just occurred, resume old lookahead f/ before error + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + + case 2: + // reduce + //this.reductionCount++; + + len = this.productions_[action[1]][1]; + + // perform semantic action + yyval.$ = vstack[vstack.length-len]; // default to $$ = $1 + // default location, uses first token for firsts, last for lasts + yyval._$ = { + first_line: lstack[lstack.length-(len||1)].first_line, + last_line: lstack[lstack.length-1].last_line, + first_column: lstack[lstack.length-(len||1)].first_column, + last_column: lstack[lstack.length-1].last_column + }; + if (ranges) { + yyval._$.range = [lstack[lstack.length-(len||1)].range[0], lstack[lstack.length-1].range[1]]; + } + r = this.performAction.apply(yyval, [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args)); + + if (typeof r !== 'undefined') { + return r; + } + + // pop off stack + if (len) { + stack = stack.slice(0,-1*len*2); + vstack = vstack.slice(0, -1*len); + lstack = lstack.slice(0, -1*len); + } + + stack.push(this.productions_[action[1]][0]); // push nonterminal (reduce) + vstack.push(yyval.$); + lstack.push(yyval._$); + // goto new state = table[STATE][NONTERMINAL] + newState = table[stack[stack.length-2]][stack[stack.length-1]]; + stack.push(newState); + break; + + case 3: + // accept + return true; + } + + } + + return true; +}}; + +/* generated by jison-lex 0.3.4 */ +var lexer = (function(){ +var lexer = ({ + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input, yy) { + this.yy = yy || this.yy || {}; + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function(match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex () { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin (condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState () { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules () { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState (n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState (condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:/* skip whitespace */ +break; +case 1:return 8; +break; +case 2:return 8; +break; +case 3:return 21; +break; +case 4:return 35; +break; +case 5:return 24; +break; +case 6:return 26; +break; +case 7:return 26; +break; +case 8:return 25; +break; +case 9:return 21; +break; +case 10:return 31; +break; +case 11:return 31; +break; +case 12:return 33; +break; +case 13:return 28; +break; +case 14:return 9; +break; +case 15:return ' '; +break; +case 16:return 32; +break; +case 17:return 27; +break; +case 18:return 29; +break; +case 19:return 30; +break; +case 20:return 18; +break; +case 21:return 19; +break; +case 22:return 17; +break; +case 23:return 11; +break; +case 24:return 20; +break; +case 25:return 12; +break; +case 26:return 13; +break; +case 27:return 15; +break; +case 28:return 14; +break; +case 29:return 16; +break; +case 30:return '"'; +break; +case 31:return "'"; +break; +case 32:return "!"; +break; +case 33:return 10; +break; +case 34:return 34; +break; +case 35:return '#'; +break; +case 36:return 5; +break; +} +}, +rules: [/^(?:\s+)/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:[A-Za-z]{1,}[A-Za-z_0-9\.]+(?=[(]))/,/^(?:#[A-Z0-9\/]+(!|\?)?)/,/^(?:\$[A-Za-z]+\$[0-9]+)/,/^(?:\$[A-Za-z]+[0-9]+)/,/^(?:[A-Za-z]+\$[0-9]+)/,/^(?:[A-Za-z]+[0-9]+)/,/^(?:[A-Za-z\.]+(?=[(]))/,/^(?:[A-Za-z]{1,}[A-Za-z_0-9]+)/,/^(?:[A-Za-z_]+)/,/^(?:[0-9]+)/,/^(?:\[(.*)?\])/,/^(?:&)/,/^(?: )/,/^(?:[.])/,/^(?::)/,/^(?:;)/,/^(?:,)/,/^(?:\*)/,/^(?:\/)/,/^(?:-)/,/^(?:\+)/,/^(?:\^)/,/^(?:\()/,/^(?:\))/,/^(?:>)/,/^(?:<)/,/^(?:NOT\b)/,/^(?:")/,/^(?:')/,/^(?:!)/,/^(?:=)/,/^(?:%)/,/^(?:[#])/,/^(?:$)/], +conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"inclusive":true}} +}); +return lexer; +})(); +parser.lexer = lexer; +function Parser () { + this.yy = {}; +} +Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); diff --git a/benchmarks/pairs/vfc-js/hot-formula-parser-3-0-0/vuln.js b/benchmarks/pairs/vfc-js/hot-formula-parser-3-0-0/vuln.js new file mode 100644 index 0000000..2b9568b --- /dev/null +++ b/benchmarks/pairs/vfc-js/hot-formula-parser-3-0-0/vuln.js @@ -0,0 +1,876 @@ +// Provenance: handsontable/formula-parser (vuln). +// repo: handsontable/formula-parser +// commit: 944136096f44ff4ffd4ba0bcef30970ca4702fce +// parent: 944136096f44ff4ffd4ba0bcef30970ca4702fce +// commit_url: https://github.com/handsontable/formula-parser/commit/396b089738d4bf30eb570a4fe6a188affa95cd5e +// cve: CVE-2020-6836 +// license: MIT +// function: performAction +// relpath: src/grammar-parser/grammar-parser.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 74 + +var grammarParser = (function(){ +var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,5],$V1=[1,8],$V2=[1,6],$V3=[1,7],$V4=[1,9],$V5=[1,14],$V6=[1,15],$V7=[1,16],$V8=[1,12],$V9=[1,13],$Va=[1,17],$Vb=[1,19],$Vc=[1,20],$Vd=[1,21],$Ve=[1,22],$Vf=[1,23],$Vg=[1,24],$Vh=[1,25],$Vi=[1,26],$Vj=[1,27],$Vk=[1,28],$Vl=[5,9,10,11,13,14,15,16,17,18,19,20,29,30],$Vm=[5,9,10,11,13,14,15,16,17,18,19,20,29,30,32],$Vn=[5,9,10,11,13,14,15,16,17,18,19,20,29,30,34],$Vo=[5,10,11,13,14,15,16,17,29,30],$Vp=[5,10,13,14,15,16,29,30],$Vq=[5,10,11,13,14,15,16,17,18,19,29,30],$Vr=[13,29,30]; +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"expressions":3,"expression":4,"EOF":5,"variableSequence":6,"number":7,"STRING":8,"&":9,"=":10,"+":11,"(":12,")":13,"<":14,">":15,"NOT":16,"-":17,"*":18,"/":19,"^":20,"FUNCTION":21,"expseq":22,"cell":23,"ABSOLUTE_CELL":24,"RELATIVE_CELL":25,"MIXED_CELL":26,":":27,"ARRAY":28,";":29,",":30,"VARIABLE":31,"DECIMAL":32,"NUMBER":33,"%":34,"ERROR":35,"$accept":0,"$end":1}, +terminals_: {5:"EOF",8:"STRING",9:"&",10:"=",11:"+",12:"(",13:")",14:"<",15:">",16:"NOT",17:"-",18:"*",19:"/",20:"^",21:"FUNCTION",24:"ABSOLUTE_CELL",25:"RELATIVE_CELL",26:"MIXED_CELL",27:":",28:"ARRAY",29:";",30:",",31:"VARIABLE",32:"DECIMAL",33:"NUMBER",34:"%",35:"ERROR"}, +productions_: [0,[3,2],[4,1],[4,1],[4,1],[4,3],[4,3],[4,3],[4,3],[4,4],[4,4],[4,4],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,2],[4,2],[4,3],[4,4],[4,1],[4,1],[4,2],[23,1],[23,1],[23,1],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[23,3],[22,1],[22,1],[22,3],[22,3],[6,1],[6,3],[7,1],[7,3],[7,2],[2,1]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1: + + return $$[$0-1]; + +break; +case 2: + + this.$ = yy.callVariable($$[$0][0]); + +break; +case 3: + + this.$ = yy.toNumber($$[$0]); + +break; +case 4: + + this.$ = yy.trimEdges($$[$0]); + +break; +case 5: + + this.$ = yy.evaluateByOperator('&', [$$[$0-2], $$[$0]]); + +break; +case 6: + + this.$ = yy.evaluateByOperator('=', [$$[$0-2], $$[$0]]); + +break; +case 7: + + this.$ = yy.evaluateByOperator('+', [$$[$0-2], $$[$0]]); + +break; +case 8: + + this.$ = $$[$0-1]; + +break; +case 9: + + this.$ = yy.evaluateByOperator('<=', [$$[$0-3], $$[$0]]); + +break; +case 10: + + this.$ = yy.evaluateByOperator('>=', [$$[$0-3], $$[$0]]); + +break; +case 11: + + this.$ = yy.evaluateByOperator('<>', [$$[$0-3], $$[$0]]); + +break; +case 12: + + this.$ = yy.evaluateByOperator('NOT', [$$[$0-2], $$[$0]]); + +break; +case 13: + + this.$ = yy.evaluateByOperator('>', [$$[$0-2], $$[$0]]); + +break; +case 14: + + this.$ = yy.evaluateByOperator('<', [$$[$0-2], $$[$0]]); + +break; +case 15: + + this.$ = yy.evaluateByOperator('-', [$$[$0-2], $$[$0]]); + +break; +case 16: + + this.$ = yy.evaluateByOperator('*', [$$[$0-2], $$[$0]]); + +break; +case 17: + + this.$ = yy.evaluateByOperator('/', [$$[$0-2], $$[$0]]); + +break; +case 18: + + this.$ = yy.evaluateByOperator('^', [$$[$0-2], $$[$0]]); + +break; +case 19: + + var n1 = yy.invertNumber($$[$0]); + + this.$ = n1; + + if (isNaN(this.$)) { + this.$ = 0; + } + +break; +case 20: + + var n1 = yy.toNumber($$[$0]); + + this.$ = n1; + + if (isNaN(this.$)) { + this.$ = 0; + } + +break; +case 21: + + this.$ = yy.callFunction($$[$0-2]); + +break; +case 22: + + this.$ = yy.callFunction($$[$0-3], $$[$0-1]); + +break; +case 26: case 27: case 28: + + this.$ = yy.cellValue($$[$0]); + +break; +case 29: case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: + + this.$ = yy.rangeValue($$[$0-2], $$[$0]); + +break; +case 38: case 42: + + this.$ = [$$[$0]]; + +break; +case 39: + + var result = []; + var arr = eval("[" + yytext + "]"); + + arr.forEach(function(item) { + result.push(item); + }); + + this.$ = result; + +break; +case 40: case 41: + + $$[$0-2].push($$[$0]); + this.$ = $$[$0-2]; + +break; +case 43: + + this.$ = (Array.isArray($$[$0-2]) ? $$[$0-2] : [$$[$0-2]]); + this.$.push($$[$0]); + +break; +case 44: + + this.$ = $$[$0]; + +break; +case 45: + + this.$ = ($$[$0-2] + '.' + $$[$0]) * 1; + +break; +case 46: + + this.$ = $$[$0-1] * 0.01; + +break; +case 47: + + this.$ = yy.throwError($$[$0]); + +break; +} +}, +table: [{2:11,3:1,4:2,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{1:[3]},{5:[1,18],9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk},o($Vl,[2,2],{32:[1,29]}),o($Vl,[2,3],{34:[1,30]}),o($Vl,[2,4]),{2:11,4:31,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:32,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:33,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{12:[1,34]},o($Vl,[2,23]),o($Vl,[2,24],{2:35,35:$Va}),o($Vm,[2,42]),o($Vn,[2,44],{32:[1,36]}),o($Vl,[2,26],{27:[1,37]}),o($Vl,[2,27],{27:[1,38]}),o($Vl,[2,28],{27:[1,39]}),o([5,9,10,11,13,14,15,16,17,18,19,20,29,30,35],[2,47]),{1:[2,1]},{2:11,4:40,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:41,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:42,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:45,6:3,7:4,8:$V0,10:[1,43],11:$V1,12:$V2,15:[1,44],17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:47,6:3,7:4,8:$V0,10:[1,46],11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:48,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:49,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:50,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:51,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:52,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{31:[1,53]},o($Vn,[2,46]),{9:$Vb,10:$Vc,11:$Vd,13:[1,54],14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk},o($Vo,[2,19],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),o($Vo,[2,20],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),{2:11,4:57,6:3,7:4,8:$V0,11:$V1,12:$V2,13:[1,55],17:$V3,21:$V4,22:56,23:10,24:$V5,25:$V6,26:$V7,28:[1,58],31:$V8,33:$V9,35:$Va},o($Vl,[2,25]),{33:[1,59]},{24:[1,60],25:[1,61],26:[1,62]},{24:[1,63],25:[1,64],26:[1,65]},{24:[1,66],25:[1,67],26:[1,68]},o($Vl,[2,5]),o([5,10,13,29,30],[2,6],{9:$Vb,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vo,[2,7],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),{2:11,4:69,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:70,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},o($Vp,[2,14],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),{2:11,4:71,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},o($Vp,[2,13],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o([5,10,13,16,29,30],[2,12],{9:$Vb,11:$Vd,14:$Ve,15:$Vf,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vo,[2,15],{9:$Vb,18:$Vi,19:$Vj,20:$Vk}),o($Vq,[2,16],{9:$Vb,20:$Vk}),o($Vq,[2,17],{9:$Vb,20:$Vk}),o([5,10,11,13,14,15,16,17,18,19,20,29,30],[2,18],{9:$Vb}),o($Vm,[2,43]),o($Vl,[2,8]),o($Vl,[2,21]),{13:[1,72],29:[1,73],30:[1,74]},o($Vr,[2,38],{9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vr,[2,39]),o($Vn,[2,45]),o($Vl,[2,29]),o($Vl,[2,30]),o($Vl,[2,31]),o($Vl,[2,32]),o($Vl,[2,33]),o($Vl,[2,34]),o($Vl,[2,35]),o($Vl,[2,36]),o($Vl,[2,37]),o($Vp,[2,9],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vp,[2,11],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vp,[2,10],{9:$Vb,11:$Vd,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vl,[2,22]),{2:11,4:75,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},{2:11,4:76,6:3,7:4,8:$V0,11:$V1,12:$V2,17:$V3,21:$V4,23:10,24:$V5,25:$V6,26:$V7,31:$V8,33:$V9,35:$Va},o($Vr,[2,40],{9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk}),o($Vr,[2,41],{9:$Vb,10:$Vc,11:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk})], +defaultActions: {18:[2,1]}, +parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + function _parseError (msg, hash) { + this.message = msg; + this.hash = hash; + } + _parseError.prototype = Error; + + throw new _parseError(str, hash); + } +}, +parse: function parse(input) { + var self = this, + stack = [0], + tstack = [], // token stack + vstack = [null], // semantic value stack + lstack = [], // location stack + table = this.table, + yytext = '', + yylineno = 0, + yyleng = 0, + recovering = 0, + TERROR = 2, + EOF = 1; + + var args = lstack.slice.call(arguments, 1); + + //this.reductionCount = this.shiftCount = 0; + + var lexer = Object.create(this.lexer); + var sharedState = { yy: {} }; + // copy state + for (var k in this.yy) { + if (Object.prototype.hasOwnProperty.call(this.yy, k)) { + sharedState.yy[k] = this.yy[k]; + } + } + + lexer.setInput(input, sharedState.yy); + sharedState.yy.lexer = lexer; + sharedState.yy.parser = this; + if (typeof lexer.yylloc == 'undefined') { + lexer.yylloc = {}; + } + var yyloc = lexer.yylloc; + lstack.push(yyloc); + + var ranges = lexer.options && lexer.options.ranges; + + if (typeof sharedState.yy.parseError === 'function') { + this.parseError = sharedState.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + + function popStack (n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + + var lex = function () { + var token; + token = lexer.lex() || EOF; + // if token isn't its numeric value, convert + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + } + + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + // retreive state number from top of stack + state = stack[stack.length - 1]; + + // use default actions if available + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + // read action for current state and first input + action = table[state] && table[state][symbol]; + } + + // handle parse error + if (typeof action === 'undefined' || !action.length || !action[0]) { + var error_rule_depth; + var errStr = ''; + + // Return the rule stack depth where the nearest error rule can be found. + // Return FALSE when no error recovery rule was found. + function locateNearestErrorRecoveryRule(state) { + var stack_probe = stack.length - 1; + var depth = 0; + + // try to recover from error + for(;;) { + // check for error recovery rule in this state + if ((TERROR.toString()) in table[state]) { + return depth; + } + if (state === 0 || stack_probe < 2) { + return false; // No suitable error recovery rule available. + } + stack_probe -= 2; // popStack(1): [symbol, action] + state = stack[stack_probe]; + ++depth; + } + } + + if (!recovering) { + // first see if there's any chance at hitting an error recovery rule: + error_rule_depth = locateNearestErrorRecoveryRule(state); + + // Report error + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push("'"+this.terminals_[p]+"'"); + } + } + if (lexer.showPosition) { + errStr = 'Parse error on line '+(yylineno+1)+":\n"+lexer.showPosition()+"\nExpecting "+expected.join(', ') + ", got '" + (this.terminals_[symbol] || symbol)+ "'"; + } else { + errStr = 'Parse error on line '+(yylineno+1)+": Unexpected " + + (symbol == EOF ? "end of input" : + ("'"+(this.terminals_[symbol] || symbol)+"'")); + } + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected, + recoverable: (error_rule_depth !== false) + }); + } else if (preErrorSymbol !== EOF) { + error_rule_depth = locateNearestErrorRecoveryRule(state); + } + + // just recovered from another error + if (recovering == 3) { + if (symbol === EOF || preErrorSymbol === EOF) { + throw new Error(errStr || 'Parsing halted while starting to recover from another error.'); + } + + // discard current lookahead and grab another + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + symbol = lex(); + } + + // try to recover from error + if (error_rule_depth === false) { + throw new Error(errStr || 'Parsing halted. No suitable error recovery rule available.'); + } + popStack(error_rule_depth); + + preErrorSymbol = (symbol == TERROR ? null : symbol); // save the lookahead token + symbol = TERROR; // insert generic error symbol as new lookahead + state = stack[stack.length-1]; + action = table[state] && table[state][TERROR]; + recovering = 3; // allow 3 real symbols to be shifted before reporting a new error + } + + // this shouldn't happen, unless resolve defaults are off + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: '+state+', token: '+symbol); + } + + switch (action[0]) { + case 1: // shift + //this.shiftCount++; + + stack.push(symbol); + vstack.push(lexer.yytext); + lstack.push(lexer.yylloc); + stack.push(action[1]); // push state + symbol = null; + if (!preErrorSymbol) { // normal execution/no error + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + // error just occurred, resume old lookahead f/ before error + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + + case 2: + // reduce + //this.reductionCount++; + + len = this.productions_[action[1]][1]; + + // perform semantic action + yyval.$ = vstack[vstack.length-len]; // default to $$ = $1 + // default location, uses first token for firsts, last for lasts + yyval._$ = { + first_line: lstack[lstack.length-(len||1)].first_line, + last_line: lstack[lstack.length-1].last_line, + first_column: lstack[lstack.length-(len||1)].first_column, + last_column: lstack[lstack.length-1].last_column + }; + if (ranges) { + yyval._$.range = [lstack[lstack.length-(len||1)].range[0], lstack[lstack.length-1].range[1]]; + } + r = this.performAction.apply(yyval, [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args)); + + if (typeof r !== 'undefined') { + return r; + } + + // pop off stack + if (len) { + stack = stack.slice(0,-1*len*2); + vstack = vstack.slice(0, -1*len); + lstack = lstack.slice(0, -1*len); + } + + stack.push(this.productions_[action[1]][0]); // push nonterminal (reduce) + vstack.push(yyval.$); + lstack.push(yyval._$); + // goto new state = table[STATE][NONTERMINAL] + newState = table[stack[stack.length-2]][stack[stack.length-1]]; + stack.push(newState); + break; + + case 3: + // accept + return true; + } + + } + + return true; +}}; + +/* generated by jison-lex 0.3.4 */ +var lexer = (function(){ +var lexer = ({ + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input, yy) { + this.yy = yy || this.yy || {}; + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:/* skip whitespace */ +break; +case 1:return 8; +break; +case 2:return 8; +break; +case 3:return 21; +break; +case 4:return 35; +break; +case 5:return 24; +break; +case 6:return 26; +break; +case 7:return 26; +break; +case 8:return 25; +break; +case 9:return 21; +break; +case 10:return 31; +break; +case 11:return 31; +break; +case 12:return 33; +break; +case 13:return 28; +break; +case 14:return 9; +break; +case 15:return ' '; +break; +case 16:return 32; +break; +case 17:return 27; +break; +case 18:return 29; +break; +case 19:return 30; +break; +case 20:return 18; +break; +case 21:return 19; +break; +case 22:return 17; +break; +case 23:return 11; +break; +case 24:return 20; +break; +case 25:return 12; +break; +case 26:return 13; +break; +case 27:return 15; +break; +case 28:return 14; +break; +case 29:return 16; +break; +case 30:return '"'; +break; +case 31:return "'"; +break; +case 32:return "!"; +break; +case 33:return 10; +break; +case 34:return 34; +break; +case 35:return '#'; +break; +case 36:return 5; +break; +} +}, +rules: [/^(?:\s+)/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:[A-Za-z]{1,}[A-Za-z_0-9\.]+(?=[(]))/,/^(?:#[A-Z0-9\/]+(!|\?)?)/,/^(?:\$[A-Za-z]+\$[0-9]+)/,/^(?:\$[A-Za-z]+[0-9]+)/,/^(?:[A-Za-z]+\$[0-9]+)/,/^(?:[A-Za-z]+[0-9]+)/,/^(?:[A-Za-z\.]+(?=[(]))/,/^(?:[A-Za-z]{1,}[A-Za-z_0-9]+)/,/^(?:[A-Za-z_]+)/,/^(?:[0-9]+)/,/^(?:\[(.*)?\])/,/^(?:&)/,/^(?: )/,/^(?:[.])/,/^(?::)/,/^(?:;)/,/^(?:,)/,/^(?:\*)/,/^(?:\/)/,/^(?:-)/,/^(?:\+)/,/^(?:\^)/,/^(?:\()/,/^(?:\))/,/^(?:>)/,/^(?:<)/,/^(?:NOT\b)/,/^(?:")/,/^(?:')/,/^(?:!)/,/^(?:=)/,/^(?:%)/,/^(?:[#])/,/^(?:$)/], +conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"inclusive":true}} +}); +return lexer; +})(); +parser.lexer = lexer; +function Parser () { + this.yy = {}; +} +Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); diff --git a/benchmarks/pairs/vfc-js/libnmap-0-4-11/fixed.js b/benchmarks/pairs/vfc-js/libnmap-0-4-11/fixed.js new file mode 100644 index 0000000..7898ee7 --- /dev/null +++ b/benchmarks/pairs/vfc-js/libnmap-0-4-11/fixed.js @@ -0,0 +1,234 @@ +// Provenance: jas-/node-libnmap (fixed). +// repo: jas-/node-libnmap +// commit: c235b24b0342e12a5dbf502f89daabe95883b6c8 +// parent: 49b72239b144ab7ffdf673b62e18bfd0760948f6 +// commit_url: https://github.com/jas-/node-libnmap/commit/c235b24b0342e12a5dbf502f89daabe95883b6c8 +// cve: CVE-2018-16461 +// license: MIT +// function: block +// relpath: lib/classes/tools.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 20 + +class tools extends emitter { + + /** + * @function merge + * Perform preliminary option/default object merge + * + * @param {Object} defaults Application defaults + * @param {Object} obj User supplied object + * + * @returns {Object} + */ + merge(defaults, obj) { + return merge(defaults, obj); + } + + + /** + * @function chunk + * Defines new property for array's + * + * @param {Array} obj Supplied array + * @param {Integer} offset Supplied offset + * + * @returns {Array} + */ + chunk(obj, offset) { + let idx = 0; + const alength = obj.length; + const tarray = []; + + for (idx = 0; idx < alength; idx += offset) { + tarray.push(obj.slice(idx, idx + offset).join(' ')); + } + + return tarray; + } + + + /** + * @function flatten + * Flattens nested arrays into one flat array + * + * @param {Array} arr Array combinator + * @param {Array} obj User supplied array + * + * @returns {Array} + */ + flatten(arr, obj) { + let value; + const result = []; + + for (let i = 0, length = arr.length; i < length; i++) { + + value = arr[i]; + + if (Array.isArray(value)) { + return this.flatten(value, obj); + } + else { + result.push(value); + } + } + return result; + } + + + /** + * @function funcs + * Create functions for use as callbacks + * + * @param {Obect} opts Application defaults + * + * @returns {Array} + */ + funcs(opts) { + const scope = this; + const funcs = {}; + let cmd = false; + const errors = []; + const reports = []; + + if (opts.range.length <= 0) + return "Range of hosts could not be created"; + + Object.keys(opts.range).forEach(function blocks(block) { + + // Acquire an array of ranges + const range = opts.range[block]; + + // Create a new function for each range block + funcs[range] = function block(callback) { + const report = []; + + + // Capture the flags, & ranges as a command string + cmd = scope.command(opts, range); + + // Split up cmd for .spawn() + const obj = cmd.split(" "); + cmd = obj[0]; + const args = obj.slice(1); + + if (opts.verbose) + console.log(`Running: ${cmd}`); + + // Push it on to the heap + const execute = proc(cmd, args); + + // Discard stderr for now, maybe log? + execute.stderr.on('data', (chunk) => { + /* Silently discard stderr messages to not interupt scans */ + }); + + // Push to array of reports + execute.stdout.on('data', (chunk) => { + report.push(chunk); + }); + + // Push report array to reporting function on stream end + execute.stdout.on('end', () => { + if (report.length > 0) + return reporting.reports(opts, report, callback); + }); + }; + }); + + return funcs; + } + + + /** + * @function command + * Generate nmap command string + * + * @param {Object} opts - User supplied options + * @param {String} block - Network block + * + * @returns {String} NMAP scan string + */ + command(opts, block) { + const flags = opts.flags.join(' '); + const proto = (opts.udp) ? ' -sU' : ' '; + const to = `--host-timeout=${opts.timeout}s `; + const ipv6 = (validation.test(validation.patterns.IPv6, block)) ? + ' -6 ' : ' '; + + return (opts.ports) ? + `${opts.nmap+proto} ${to}${flags}${ipv6}-p${opts.ports} ${block}` : + `${opts.nmap+proto} ${to}${flags}${ipv6}${block}`; + } + + + /** + * @function worker + * Executes object of functions + * + * @param {Object} obj User supplied object + * @param {Function} fn Return function + */ + worker(obj, fn) { + async.parallelLimit(obj.funcs, obj.threshold, fn); + } + + + /** + * @function init + * @scope private + * Merges supplied options & builds functions + * + * @param {Object} defaults libnmap default options + * @param {Object} opts User supplied configuration object + * @param {Function} cb Callback + * + * @returns {Object} + */ + init(defaults, opts, cb) { + let funcs = []; + const ranges = []; + const called = caller.get()[1].getFunctionName(); + + /* Override 'defaults.flags' array with 'opts.flags' (prevents merge) */ + if (/array/.test(typeof opts.flags)) + defaults.flags = opts.flags; + + opts = this.merge(defaults, opts); + + /* Ensure we can always parse the report */ + if (opts.flags.indexOf('-oX -') === -1) + opts.flags.push('-oX -'); + + /* If we are in a discover mode get adapters for associated network(s) */ + if (/nmap.discover/.test(called)) { + + /* Bail early if we cannot determine any ranges */ + if (!(opts.range = network.adapters(opts))) + return cb(validation.messages.version); + + /* Set scan options as static values for 'discover' mode */ + opts.ports = ''; + opts.flags = [ + '-n', + '-oX -', + '-sn', + '-PR' + ]; + } + + validation.init(opts, (err, result) => { + if (err) + return cb(err); + + opts.range = network.calculate(opts); + funcs = this.funcs(opts); + + return cb(null, { + opts, + funcs + }); + }); + } +} diff --git a/benchmarks/pairs/vfc-js/libnmap-0-4-11/vuln.js b/benchmarks/pairs/vfc-js/libnmap-0-4-11/vuln.js new file mode 100644 index 0000000..13a7313 --- /dev/null +++ b/benchmarks/pairs/vfc-js/libnmap-0-4-11/vuln.js @@ -0,0 +1,222 @@ +// Provenance: jas-/node-libnmap (vuln). +// repo: jas-/node-libnmap +// commit: 49b72239b144ab7ffdf673b62e18bfd0760948f6 +// parent: 49b72239b144ab7ffdf673b62e18bfd0760948f6 +// commit_url: https://github.com/jas-/node-libnmap/commit/c235b24b0342e12a5dbf502f89daabe95883b6c8 +// cve: CVE-2018-16461 +// license: MIT +// function: block +// relpath: lib/classes/tools.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 20 + +class tools extends emitter { + + /** + * @function merge + * Perform preliminary option/default object merge + * + * @param {Object} defaults Application defaults + * @param {Object} obj User supplied object + * + * @returns {Object} + */ + merge(defaults, obj) { + return merge(defaults, obj); + } + + + /** + * @function chunk + * Defines new property for array's + * + * @param {Array} obj Supplied array + * @param {Integer} offset Supplied offset + * + * @returns {Array} + */ + chunk(obj, offset) { + let idx = 0; + const alength = obj.length; + const tarray = []; + + for (idx = 0; idx < alength; idx += offset) { + tarray.push(obj.slice(idx, idx + offset).join(' ')); + } + + return tarray; + } + + + /** + * @function flatten + * Flattens nested arrays into one flat array + * + * @param {Array} arr Array combinator + * @param {Array} obj User supplied array + * + * @returns {Array} + */ + flatten(arr, obj) { + let value; + const result = []; + + for (let i = 0, length = arr.length; i < length; i++) { + + value = arr[i]; + + if (Array.isArray(value)) { + return this.flatten(value, obj); + } else { + result.push(value); + } + } + return result; + } + + + /** + * @function funcs + * Create functions for use as callbacks + * + * @param {Obect} opts Application defaults + * + * @returns {Array} + */ + funcs(opts) { + const scope = this; + const funcs = {}; + let cmd = false; + const errors = []; + const reports = []; + + if (opts.range.length <= 0) + return "Range of hosts could not be created"; + + Object.keys(opts.range).forEach(function blocks(block) { + + const range = opts.range[block]; + funcs[range] = function block(callback) { + cmd = scope.command(opts, range); + + if (opts.verbose) + console.log(`Running: ${cmd}`); + + const report = []; + + const execute = proc(cmd, (err, stdout, stderr) => { + if (err) + return reporting.reports(opts, err, callback); + }); + + execute.stderr.on('data', (chunk) => { + /* Silently discard stderr messages to not interupt scans */ + }); + + execute.stdout.on('data', (chunk) => { + report.push(chunk); + }); + + execute.stdout.on('end', () => { + if (report.length > 0) + return reporting.reports(opts, report, callback); + }); + }; + }); + + return funcs; + } + + + /** + * @function command + * Generate nmap command string + * + * @param {Object} opts - User supplied options + * @param {String} block - Network block + * + * @returns {String} NMAP scan string + */ + command(opts, block) { + const flags = opts.flags.join(' '); + const proto = (opts.udp) ? ' -sU' : ' '; + const to = `--host-timeout=${opts.timeout}s `; + const ipv6 = (validation.test(validation.patterns.IPv6, block)) ? + ' -6 ' : ' '; + + return (opts.ports) ? + `${opts.nmap+proto} ${to}${flags}${ipv6}-p${opts.ports} ${block}` : + `${opts.nmap+proto} ${to}${flags}${ipv6}${block}`; + } + + + /** + * @function worker + * Executes object of functions + * + * @param {Object} obj User supplied object + * @param {Function} fn Return function + */ + worker(obj, fn) { + async.parallelLimit(obj.funcs, obj.threshold, fn); + } + + + /** + * @function init + * @scope private + * Merges supplied options & builds functions + * + * @param {Object} defaults libnmap default options + * @param {Object} opts User supplied configuration object + * @param {Function} cb Callback + * + * @returns {Object} + */ + init(defaults, opts, cb) { + let funcs = []; + const ranges = []; + const called = caller.get()[1].getFunctionName(); + + /* Override 'defaults.flags' array with 'opts.flags' (prevents merge) */ + if (/array/.test(typeof opts.flags)) + defaults.flags = opts.flags; + + opts = this.merge(defaults, opts); + + /* Ensure we can always parse the report */ + if (opts.flags.indexOf('-oX -') === -1) + opts.flags.push('-oX -'); + + /* If we are in a discover mode get adapters for associated network(s) */ + if (/nmap.discover/.test(called)) { + + /* Bail early if we cannot determine any ranges */ + if (!(opts.range = network.adapters(opts))) + return cb(validation.messages.version); + + /* Set scan options as static values for 'discover' mode */ + opts.ports = ''; + opts.flags = [ + '-n', + '-oX -', + '-sn', + '-PR' + ]; + } + + validation.init(opts, (err, result) => { + if (err) + return cb(err); + + opts.range = network.calculate(opts); + funcs = this.funcs(opts); + + return cb(null, { + opts, + funcs + }); + }); + } +} diff --git a/benchmarks/pairs/vfc-js/pdf-image-1-0-5/fixed.js b/benchmarks/pairs/vfc-js/pdf-image-1-0-5/fixed.js new file mode 100644 index 0000000..f6f4d6b --- /dev/null +++ b/benchmarks/pairs/vfc-js/pdf-image-1-0-5/fixed.js @@ -0,0 +1,214 @@ +// Provenance: roest01/node-pdf-image (fixed). +// repo: roest01/node-pdf-image +// commit: 54679496a89738443917608c2bbe2f6e5dd20e83 +// parent: d8c0dcadd93985dbefe80b84060f5f572bfbb6ff +// commit_url: https://github.com/roest01/node-pdf-image/commit/54679496a89738443917608c2bbe2f6e5dd20e83 +// cve: CVE-2018-3757 +// license: MIT +// function: getInfo +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 24 + +PDFImage.prototype = { + constructGetInfoCommand: function () { + return { + cmd: "pdfinfo", + args: [this.pdfFilePath] + }; + }, + parseGetInfoCommandOutput: function (output) { + var info = {}; + output.split("\n").forEach(function (line) { + if (line.match(/^(.*?):[ \t]*(.*)$/)) { + info[RegExp.$1] = RegExp.$2; + } + }); + return info; + }, + getInfo: function () { + var self = this; + var getInfoCommand = this.constructGetInfoCommand(); + return new Promise(function (resolve, reject) { + spawn(getInfoCommand.cmd, getInfoCommand.args, { capture: [ 'stdout', 'stderr' ]}) + .then(function (cmdResult) { + resolve(self.parseGetInfoCommandOutput(cmdResult.stdout.toString())); + }).catch(reject); + }); + }, + numberOfPages: function () { + return this.getInfo().then(function (info) { + return info["Pages"]; + }); + }, + getOutputImagePathForPage: function (pageNumber) { + return path.join( + this.outputDirectory, + this.pdfFileBaseName + "-" + pageNumber + "." + this.convertExtension + ); + }, + getOutputImagePathForFile: function () { + return path.join( + this.outputDirectory, + this.pdfFileBaseName + "." + this.convertExtension + ); + }, + setConvertOptions: function (convertOptions) { + this.convertOptions = convertOptions || {}; + }, + setPdfFileBaseName: function(pdfFileBaseName) { + this.pdfFileBaseName = pdfFileBaseName || path.basename(this.pdfFilePath, ".pdf"); + }, + setConvertExtension: function (convertExtension) { + this.convertExtension = convertExtension || "png"; + }, + constructConvertCommandForPage: function (pageNumber) { + var pdfFilePath = this.pdfFilePath; + var outputImagePath = this.getOutputImagePathForPage(pageNumber); + var convertOptions = this.constructConvertOptions(); + var args = []; + if (convertOptions) args = convertOptions.slice(); + args.push(pdfFilePath+"["+pageNumber+"]"); + args.push(outputImagePath); + + return { + cmd: this.useGM ? "gm convert" : "convert", + args: args + }; + }, + constructCombineCommandForFile: function (imagePaths) { + var args = imagePaths.slice(); + args.push(this.getOutputImagePathForFile()); + args.unshift("-append"); + return { + cmd: this.useGM ? "gm convert" : "convert", + args: args + }; + }, + constructConvertOptions: function () { + var convertOptions = []; + Object.keys(this.convertOptions).sort().map(function (optionName) { + if (this.convertOptions[optionName] !== null) { + convertOptions.push(optionName); + convertOptions.push(this.convertOptions[optionName]); + } else { + convertOptions.push(optionName); + } + }, this); + return convertOptions; + }, + combineImages: function(imagePaths) { + var pdfImage = this; + var combineCommand = pdfImage.constructCombineCommandForFile(imagePaths); + return new Promise(function (resolve, reject) { + spawn(combineCommand.cmd, combineCommand.args, { capture: [ 'stdout', 'stderr' ]}) + .then(function () { + spawn("rm", imagePaths); //cleanUp + resolve(pdfImage.getOutputImagePathForFile()); + }).catch(function(error){ + reject({ + message: "Failed to combine images", + error: error.message, + stdout: error.stdout, + stderr: error.stderr + }); + }); + }); + }, + convertFile: function () { + var pdfImage = this; + return new Promise(function (resolve, reject) { + pdfImage.numberOfPages().then(function (totalPages) { + var convertPromise = new Promise(function (resolve, reject){ + var imagePaths = []; + for (var i = 0; i < totalPages; i++) { + pdfImage.convertPage(i).then(function(imagePath){ + imagePaths.push(imagePath); + if (imagePaths.length === parseInt(totalPages)){ + imagePaths.sort(); //because of asyc pages we have to reSort pages + resolve(imagePaths); + } + }).catch(function(error){ + reject(error); + }); + } + }); + + convertPromise.then(function(imagePaths){ + if (pdfImage.combinedImage){ + pdfImage.combineImages(imagePaths).then(function(imagePath){ + resolve(imagePath); + }); + } else { + resolve(imagePaths); + } + }).catch(function(error){ + reject(error); + }); + }); + }); + }, + convertPage: function (pageNumber) { + var pdfFilePath = this.pdfFilePath; + var outputImagePath = this.getOutputImagePathForPage(pageNumber); + var convertCommand = this.constructConvertCommandForPage(pageNumber); + + var promise = new Promise(function (resolve, reject) { + function convertPageToImage() { + return new Promise(function (resolve, reject) { + spawn(convertCommand.cmd, convertCommand.args, { capture: [ 'stdout', 'stderr' ]}) + .then(function () { + resolve(outputImagePath); + }).catch(function(error){ + reject({ + message: "Failed to convert page to image", + error: error.message, + stdout: error.stdout, + stderr: error.stderr + }); + }); + }); + } + + fs.stat(outputImagePath, function (err, imageFileStat) { + var imageNotExists = err && err.code === "ENOENT"; + if (!imageNotExists && err) { + return reject({ + message: "Failed to stat image file", + error: err + }); + } + + // convert when (1) image doesn't exits or (2) image exists + // but its timestamp is older than pdf's one + + if (imageNotExists) { + // (1) + convertPageToImage().then(function(result){ + resolve(result); + }).catch(reject); + return; + } + + // image exist. check timestamp. + fs.stat(pdfFilePath, function (err, pdfFileStat) { + if (err) { + return reject({ + message: "Failed to stat PDF file", + error: err + }); + } + + if (imageFileStat.mtime < pdfFileStat.mtime) { + // (2) + convertPageToImage().then(function(result){ + resolve(result); + }).catch(reject); + } + }); + }); + }); + return promise; + } +}; diff --git a/benchmarks/pairs/vfc-js/pdf-image-1-0-5/vuln.js b/benchmarks/pairs/vfc-js/pdf-image-1-0-5/vuln.js new file mode 100644 index 0000000..159b6b8 --- /dev/null +++ b/benchmarks/pairs/vfc-js/pdf-image-1-0-5/vuln.js @@ -0,0 +1,212 @@ +// Provenance: roest01/node-pdf-image (vuln). +// repo: roest01/node-pdf-image +// commit: d8c0dcadd93985dbefe80b84060f5f572bfbb6ff +// parent: d8c0dcadd93985dbefe80b84060f5f572bfbb6ff +// commit_url: https://github.com/roest01/node-pdf-image/commit/54679496a89738443917608c2bbe2f6e5dd20e83 +// cve: CVE-2018-3757 +// license: MIT +// function: getInfo +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 24 + +PDFImage.prototype = { + constructGetInfoCommand: function () { + return util.format( + "pdfinfo \"%s\"", + this.pdfFilePath + ); + }, + parseGetInfoCommandOutput: function (output) { + var info = {}; + output.split("\n").forEach(function (line) { + if (line.match(/^(.*?):[ \t]*(.*)$/)) { + info[RegExp.$1] = RegExp.$2; + } + }); + return info; + }, + getInfo: function () { + var self = this; + var getInfoCommand = this.constructGetInfoCommand(); + var promise = new Promise(function (resolve, reject) { + exec(getInfoCommand, function (err, stdout, stderr) { + if (err) { + return reject({ + message: "Failed to get PDF'S information", + error: err, + stdout: stdout, + stderr: stderr + }); + } + return resolve(self.parseGetInfoCommandOutput(stdout)); + }); + }); + return promise; + }, + numberOfPages: function () { + return this.getInfo().then(function (info) { + return info["Pages"]; + }); + }, + getOutputImagePathForPage: function (pageNumber) { + return path.join( + this.outputDirectory, + this.pdfFileBaseName + "-" + pageNumber + "." + this.convertExtension + ); + }, + getOutputImagePathForFile: function () { + return path.join( + this.outputDirectory, + this.pdfFileBaseName + "." + this.convertExtension + ); + }, + setConvertOptions: function (convertOptions) { + this.convertOptions = convertOptions || {}; + }, + setPdfFileBaseName: function(pdfFileBaseName) { + this.pdfFileBaseName = pdfFileBaseName || path.basename(this.pdfFilePath, ".pdf"); + }, + setConvertExtension: function (convertExtension) { + this.convertExtension = convertExtension || "png"; + }, + constructConvertCommandForPage: function (pageNumber) { + var pdfFilePath = this.pdfFilePath; + var outputImagePath = this.getOutputImagePathForPage(pageNumber); + var convertOptionsString = this.constructConvertOptions(); + return util.format( + "%s %s\"%s[%d]\" \"%s\"", + this.useGM ? "gm convert" : "convert", + convertOptionsString ? convertOptionsString + " " : "", + pdfFilePath, pageNumber, outputImagePath + ); + }, + constructCombineCommandForFile: function (imagePaths) { + return util.format( + "%s -append %s \"%s\"", + this.useGM ? "gm convert" : "convert", + imagePaths.join(' '), + this.getOutputImagePathForFile() + ); + }, + constructConvertOptions: function () { + return Object.keys(this.convertOptions).sort().map(function (optionName) { + if (this.convertOptions[optionName] !== null) { + return optionName + " " + this.convertOptions[optionName]; + } else { + return optionName; + } + }, this).join(" "); + }, + combineImages: function(imagePaths) { + var pdfImage = this; + var combineCommand = pdfImage.constructCombineCommandForFile(imagePaths); + return new Promise(function (resolve, reject) { + exec(combineCommand, function (err, stdout, stderr) { + if (err) { + return reject({ + message: "Failed to combine images", + error: err, + stdout: stdout, + stderr: stderr + }); + } + exec("rm "+imagePaths.join(' ')); //cleanUp + return resolve(pdfImage.getOutputImagePathForFile()); + }); + }); + }, + convertFile: function () { + var pdfImage = this; + return new Promise(function (resolve, reject) { + pdfImage.numberOfPages().then(function (totalPages) { + var convertPromise = new Promise(function (resolve, reject){ + var imagePaths = []; + for (var i = 0; i < totalPages; i++) { + pdfImage.convertPage(i).then(function(imagePath){ + imagePaths.push(imagePath); + if (imagePaths.length === parseInt(totalPages)){ + imagePaths.sort(); //because of asyc pages we have to reSort pages + resolve(imagePaths); + } + }).catch(function(error){ + reject(error); + }); + } + }); + + convertPromise.then(function(imagePaths){ + if (pdfImage.combinedImage){ + pdfImage.combineImages(imagePaths).then(function(imagePath){ + resolve(imagePath); + }); + } else { + resolve(imagePaths); + } + }).catch(function(error){ + reject(error); + }); + }); + }); + }, + convertPage: function (pageNumber) { + var pdfFilePath = this.pdfFilePath; + var outputImagePath = this.getOutputImagePathForPage(pageNumber); + var convertCommand = this.constructConvertCommandForPage(pageNumber); + + var promise = new Promise(function (resolve, reject) { + function convertPageToImage() { + exec(convertCommand, function (err, stdout, stderr) { + if (err) { + return reject({ + message: "Failed to convert page to image", + error: err, + stdout: stdout, + stderr: stderr + }); + } + return resolve(outputImagePath); + }); + } + + fs.stat(outputImagePath, function (err, imageFileStat) { + var imageNotExists = err && err.code === "ENOENT"; + if (!imageNotExists && err) { + return reject({ + message: "Failed to stat image file", + error: err + }); + } + + // convert when (1) image doesn't exits or (2) image exists + // but its timestamp is older than pdf's one + + if (imageNotExists) { + // (1) + convertPageToImage(); + return; + } + + // image exist. check timestamp. + fs.stat(pdfFilePath, function (err, pdfFileStat) { + if (err) { + return reject({ + message: "Failed to stat PDF file", + error: err + }); + } + + if (imageFileStat.mtime < pdfFileStat.mtime) { + // (2) + convertPageToImage(); + return; + } + + return resolve(outputImagePath); + }); + }); + }); + return promise; + } +}; diff --git a/benchmarks/pairs/vfc-js/pidusage-1-0-0/fixed.js b/benchmarks/pairs/vfc-js/pidusage-1-0-0/fixed.js new file mode 100644 index 0000000..c1f0730 --- /dev/null +++ b/benchmarks/pairs/vfc-js/pidusage-1-0-0/fixed.js @@ -0,0 +1,198 @@ +// Provenance: soyuka/pidusage (fixed). +// repo: soyuka/pidusage +// commit: b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d +// parent: e3660d8a8e5e61afad9b02af67356a1e239d6bde +// commit_url: https://github.com/soyuka/pidusage/commit/b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d +// cve: CVE-2017-16034 +// license: MIT +// function: ps +// relpath: lib/stats.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 1 + +var os = require('os') +var fs = require('fs') +var p = require('path') +var exec = require('child_process').exec +var spawn = require('child_process').spawn +var helpers = require('./helpers') +var format = require('util').format + +var stats = { + history: {}, + cpu: null, //used to store cpu informations + proc: function(pid, options, done) { + var self = this + + if(this.cpu !== null) { + fs.readFile('/proc/uptime', 'utf8', function(err, uptime) { + if(err) { + return done(err, null) + } + + if(uptime === undefined) { + console.error("[pidusage] We couldn't find uptime from /proc/uptime") + self.cpu.uptime = os.uptime() + } else { + self.cpu.uptime = uptime.split(' ')[0] + } + + return self.proc_calc(pid, options, done) + }) + } else { + helpers.cpu(function(err, cpu) { + if(err) { + return done(err, null) + } + + self.cpu = cpu + return self.proc_calc(pid, options, done) + }) + } + }, + proc_calc: function(pid, options, done) { + pid = parseInt(pid, 10) + var history = this.history[pid] ? this.history[pid] : {} + var cpu = this.cpu + var self = this + + //Arguments to path.join must be strings + fs.readFile(p.join('/proc', ''+pid, 'stat'), 'utf8', function(err, infos) { + if(err) { + return done(err, null) + } + + //https://github.com/arunoda/node-usage/commit/a6ca74ecb8dd452c3c00ed2bde93294d7bb75aa8 + //preventing process space in name by removing values before last ) (pid (name) ...) + var index = infos.lastIndexOf(')') + infos = infos.substr(index + 2).split(' ') + + //according to http://man7.org/linux/man-pages/man5/proc.5.html (index 0 based - 2) + //In kernels before Linux 2.6, start was expressed in jiffies. Since Linux 2.6, the value is expressed in clock ticks + var stat = { + utime: parseFloat(infos[11]), + stime: parseFloat(infos[12]), + cutime: parseFloat(infos[13]), + cstime: parseFloat(infos[14]), + start: parseFloat(infos[19]) / cpu.clock_tick, + rss: parseFloat(infos[21]) + } + + //http://stackoverflow.com/questions/16726779/total-cpu-usage-of-an-application-from-proc-pid-stat/16736599#16736599 + + var childrens = options.childrens ? stat.cutime + stat.cstime : 0 + + var total = stat.stime - (history.stime || 0) + stat.utime - (history.utime || 0) + childrens + + total = total / cpu.clock_tick + + //time elapsed between calls + var seconds = history.uptime !== undefined ? cpu.uptime - history.uptime : stat.start - cpu.uptime + seconds = Math.abs(seconds) + seconds = seconds === 0 ? 1 : seconds //we sure can't divide through 0 + + self.history[pid] = stat + self.history[pid].uptime = cpu.uptime + + return done(null, { + cpu: (total / seconds) * 100, + memory: stat.rss * cpu.pagesize + }) + }) + }, + /** + * Get pid informations through ps command + * @param {int} pid + * @return {Function} done (err, stat) + * on os x skip headers with pcpu=,rss= + * on linux it could be --no-header + * on solaris 11 can't figure out a way to do this properly so... + */ + ps: function(pid, options, done) { + pid = parseInt(pid, 10) + var cmd = 'ps -o pcpu,rss -p ' + + if(os.platform() == 'aix') + cmd = 'ps -o pcpu,rssize -p ' //this one could work on other platforms + + exec(cmd + pid, function(err, stdout, stderr) { + if(err) + return done(err, null) + + stdout = stdout.split(os.EOL)[1] + stdout = stdout.replace(/^\s+/, '').replace(/\s\s+/g, ' ').split(' ') + + return done(null, { + cpu: parseFloat(stdout[0].replace(',', '.')), + memory: parseFloat(stdout[1]) * 1024 + }) + }) + }, + /** + * This is really in a beta stage + */ + win: function(pid, options, done) { + pid = parseInt(pid, 10) + var history = this.history[pid] ? this.history[pid] : {} + // http://social.msdn.microsoft.com/Forums/en-US/469ec6b7-4727-4773-9dc7-6e3de40e87b8/cpu-usage-in-for-each-active-process-how-is-this-best-determined-and-implemented-in-an?forum=csharplanguage + var args = 'PROCESS ' + pid + ' get workingsetsize,usermodetime,kernelmodetime' + + var wmic = spawn('wmic', args.split(' '), {detached: true}) + var stdout = '' + var stderr = '' + var self = this + var uptime = os.uptime() + + wmic.stdout.on('data', function(d) { + stdout += d.toString() + }) + + wmic.stderr.on('data', function(d) { + stderr += d.toString() + }) + + wmic.on('error', function(err) { + console.error('[pidusage] Command "wmic ' + args + '" failed with error %s', err) + }) + + wmic.on('close', function(code) { + stdout = stdout.trim() + stderr = stderr.trim() + + if(!stdout || code !== 0) { + var error = format('%s Wmic errored, please open an issue on https://github.com/soyuka/pidusage with this message.%s', new Date().toString(), os.EOL) + error += format('Command was "wmic %s" %s System informations: %s - release: %s %s - type %s %s', args, os.EOL, os.EOL, os.release(), os.EOL, os.type(), os.EOL) + stderr = error + (stderr ? format('Wmic reported the following error: %s.', stderr) : 'Wmic reported no errors (stderr empty).') + stderr = format('%s%s%sWmic exited with code %d.', os.EOL, stderr, os.EOL, code) + stderr = format('%s%sStdout was %s', stderr, os.EOL, stdout ? stdout : 'empty') + + return done(new Error(stderr, null)) + } + + stdout = stdout.split(os.EOL)[1].replace(/\s\s+/g, ' ').split(' ') + + var stats = { + kernelmodetime: parseFloat(stdout[0]), + usermodetime: parseFloat(stdout[1]), + } + + var workingsetsize = parseFloat(stdout[2]) + + //process usage since last call + var total = stats.kernelmodetime - (history.kernelmodetime || 0) + stats.usermodetime - (history.usermodetime || 0) + total = total / 10000000 + + //time elapsed between calls + var seconds = history.uptime !== undefined ? uptime - history.uptime : 0 + seconds = Math.abs(seconds) + seconds = seconds === 0 ? 1 : seconds //we sure can't divide through 0 + + self.history[pid] = stats + self.history[pid].uptime = uptime + + return done(null, {cpu: (total / seconds) * 100, memory: workingsetsize}) + }) + wmic.stdin.end() + } +} diff --git a/benchmarks/pairs/vfc-js/pidusage-1-0-0/vuln.js b/benchmarks/pairs/vfc-js/pidusage-1-0-0/vuln.js new file mode 100644 index 0000000..7b953b4 --- /dev/null +++ b/benchmarks/pairs/vfc-js/pidusage-1-0-0/vuln.js @@ -0,0 +1,194 @@ +// Provenance: soyuka/pidusage (vuln). +// repo: soyuka/pidusage +// commit: e3660d8a8e5e61afad9b02af67356a1e239d6bde +// parent: e3660d8a8e5e61afad9b02af67356a1e239d6bde +// commit_url: https://github.com/soyuka/pidusage/commit/b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d +// cve: CVE-2017-16034 +// license: MIT +// function: ps +// relpath: lib/stats.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 1 + +var os = require('os') +var fs = require('fs') +var p = require('path') +var exec = require('child_process').exec +var spawn = require('child_process').spawn +var helpers = require('./helpers') +var format = require('util').format + +var stats = { + history: {}, + cpu: null, //used to store cpu informations + proc: function(pid, options, done) { + var self = this + + if(this.cpu !== null) { + fs.readFile('/proc/uptime', 'utf8', function(err, uptime) { + if(err) + return done(err, null) + + if(uptime === undefined) { + console.error("[pidusage] We couldn't find uptime from /proc/uptime") + self.cpu.uptime = os.uptime() + } else { + self.cpu.uptime = uptime.split(' ')[0] + } + + return self.proc_calc(pid, options, done) + }) + } else { + helpers.cpu(function(err, cpu) { + if(err) + return done(err, null) + + self.cpu = cpu + return self.proc_calc(pid, options, done) + }) + } + }, + proc_calc: function(pid, options, done) { + var history = this.history[pid] ? this.history[pid] : {} + var cpu = this.cpu + var self = this + + //Arguments to path.join must be strings + fs.readFile(p.join('/proc', ''+pid, 'stat'), 'utf8', function(err, infos) { + + if(err) + return done(err, null) + + //https://github.com/arunoda/node-usage/commit/a6ca74ecb8dd452c3c00ed2bde93294d7bb75aa8 + //preventing process space in name by removing values before last ) (pid (name) ...) + var index = infos.lastIndexOf(')') + infos = infos.substr(index + 2).split(' ') + + //according to http://man7.org/linux/man-pages/man5/proc.5.html (index 0 based - 2) + //In kernels before Linux 2.6, start was expressed in jiffies. Since Linux 2.6, the value is expressed in clock ticks + var stat = { + utime: parseFloat(infos[11]), + stime: parseFloat(infos[12]), + cutime: parseFloat(infos[13]), + cstime: parseFloat(infos[14]), + start: parseFloat(infos[19]) / cpu.clock_tick, + rss: parseFloat(infos[21]) + } + + //http://stackoverflow.com/questions/16726779/total-cpu-usage-of-an-application-from-proc-pid-stat/16736599#16736599 + + var childrens = options.childrens ? stat.cutime + stat.cstime : 0 + + var total = stat.stime - (history.stime || 0) + stat.utime - (history.utime || 0) + childrens + + total = total / cpu.clock_tick + + //time elapsed between calls + var seconds = history.uptime !== undefined ? cpu.uptime - history.uptime : stat.start - cpu.uptime + seconds = Math.abs(seconds) + seconds = seconds === 0 ? 1 : seconds //we sure can't divide through 0 + + self.history[pid] = stat + self.history[pid].uptime = cpu.uptime + + return done(null, { + cpu: (total / seconds) * 100, + memory: stat.rss * cpu.pagesize + }) + }) + }, + /** + * Get pid informations through ps command + * @param {int} pid + * @return {Function} done (err, stat) + * on os x skip headers with pcpu=,rss= + * on linux it could be --no-header + * on solaris 11 can't figure out a way to do this properly so... + */ + ps: function(pid, options, done) { + + var cmd = 'ps -o pcpu,rss -p ' + + if(os.platform() == 'aix') + cmd = 'ps -o pcpu,rssize -p ' //this one could work on other platforms + + exec(cmd + pid, function(err, stdout, stderr) { + if(err) + return done(err, null) + + stdout = stdout.split(os.EOL)[1] + stdout = stdout.replace(/^\s+/, '').replace(/\s\s+/g, ' ').split(' ') + + return done(null, { + cpu: parseFloat(stdout[0].replace(',', '.')), + memory: parseFloat(stdout[1]) * 1024 + }) + }) + }, + /** + * This is really in a beta stage + */ + win: function(pid, options, done) { + var history = this.history[pid] ? this.history[pid] : {} + // http://social.msdn.microsoft.com/Forums/en-US/469ec6b7-4727-4773-9dc7-6e3de40e87b8/cpu-usage-in-for-each-active-process-how-is-this-best-determined-and-implemented-in-an?forum=csharplanguage + var args = 'PROCESS ' + pid + ' get workingsetsize,usermodetime,kernelmodetime' + + var wmic = spawn('wmic', args.split(' '), {detached: true}) + var stdout = '' + var stderr = '' + var self = this + var uptime = os.uptime() + + wmic.stdout.on('data', function(d) { + stdout += d.toString() + }) + + wmic.stderr.on('data', function(d) { + stderr += d.toString() + }) + + wmic.on('error', function(err) { + console.error('[pidusage] Command "wmic ' + args + '" failed with error %s', err) + }) + + wmic.on('close', function(code) { + stdout = stdout.trim() + stderr = stderr.trim() + + if(!stdout || code !== 0) { + var error = format('%s Wmic errored, please open an issue on https://github.com/soyuka/pidusage with this message.%s', new Date().toString(), os.EOL) + error += format('Command was "wmic %s" %s System informations: %s - release: %s %s - type %s %s', args, os.EOL, os.EOL, os.release(), os.EOL, os.type(), os.EOL) + stderr = error + (stderr ? format('Wmic reported the following error: %s.', stderr) : 'Wmic reported no errors (stderr empty).') + stderr = format('%s%s%sWmic exited with code %d.', os.EOL, stderr, os.EOL, code) + stderr = format('%s%sStdout was %s', stderr, os.EOL, stdout ? stdout : 'empty') + + return done(new Error(stderr, null)) + } + + stdout = stdout.split(os.EOL)[1].replace(/\s\s+/g, ' ').split(' ') + + var stats = { + kernelmodetime: parseFloat(stdout[0]), + usermodetime: parseFloat(stdout[1]), + } + + var workingsetsize = parseFloat(stdout[2]) + + //process usage since last call + var total = stats.kernelmodetime - (history.kernelmodetime || 0) + stats.usermodetime - (history.usermodetime || 0) + total = total / 10000000 + + //time elapsed between calls + var seconds = history.uptime !== undefined ? uptime - history.uptime : 0 + seconds = Math.abs(seconds) + seconds = seconds === 0 ? 1 : seconds //we sure can't divide through 0 + + self.history[pid] = stats + self.history[pid].uptime = uptime + + return done(null, {cpu: (total / seconds) * 100, memory: workingsetsize}) + }) + wmic.stdin.end() + } +} diff --git a/benchmarks/pairs/vfc-js/portprocesses-1-0-4/fixed.js b/benchmarks/pairs/vfc-js/portprocesses-1-0-4/fixed.js new file mode 100644 index 0000000..0f251eb --- /dev/null +++ b/benchmarks/pairs/vfc-js/portprocesses-1-0-4/fixed.js @@ -0,0 +1,37 @@ +// Provenance: rrainn/PortProcesses (fixed). +// repo: rrainn/PortProcesses +// commit: 86811216c9b97b01b5722f879f8c88a7aa4214e1 +// parent: fffceb09aff7180afbd0bd172e820404b33c8299 +// commit_url: https://github.com/rrainn/PortProcesses/commit/86811216c9b97b01b5722f879f8c88a7aa4214e1 +// cve: CVE-2021-23348 +// license: MIT +// function: listProcessesOnPort +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 2 + +const listProcessesOnPort = module.exports.listProcessesOnPort = async port => { + const portNumber = parseInt(port, 10); + if (Number.isNaN(portNumber)) { + console.error("Must provide number for port."); + return; + } + try { + const result = (await exec(`lsof -i :${portNumber}`)).output.split('\n'); + const headers = result.shift().split(' ').filter(item => !!item.trim() && item.trim() !== "").map(item => item.toLowerCase()); + return result.filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue) => { + accumulator.push(currentValue.split(' ').filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue, index) => { + if (index > headers.length - 1) { + accumulator[headers[headers.length - 1]] = (!!accumulator[headers[headers.length - 1]].trim() && accumulator[headers[headers.length - 1]].trim() !== "") ? `${accumulator[headers[headers.length - 1]]} ${currentValue}` : currentValue; + } else { + accumulator[headers[index]] = currentValue; + } + return accumulator; + }, {})); + return accumulator; + }, []); + } catch (e) { + console.error(e); + } +}; diff --git a/benchmarks/pairs/vfc-js/portprocesses-1-0-4/vuln.js b/benchmarks/pairs/vfc-js/portprocesses-1-0-4/vuln.js new file mode 100644 index 0000000..9485088 --- /dev/null +++ b/benchmarks/pairs/vfc-js/portprocesses-1-0-4/vuln.js @@ -0,0 +1,32 @@ +// Provenance: rrainn/PortProcesses (vuln). +// repo: rrainn/PortProcesses +// commit: fffceb09aff7180afbd0bd172e820404b33c8299 +// parent: fffceb09aff7180afbd0bd172e820404b33c8299 +// commit_url: https://github.com/rrainn/PortProcesses/commit/86811216c9b97b01b5722f879f8c88a7aa4214e1 +// cve: CVE-2021-23348 +// license: MIT +// function: listProcessesOnPort +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 2 + +const listProcessesOnPort = module.exports.listProcessesOnPort = async port => { + try { + const result = (await exec(`lsof -i :${port}`)).output.split('\n'); + const headers = result.shift().split(' ').filter(item => !!item.trim() && item.trim() !== "").map(item => item.toLowerCase()); + return result.filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue) => { + accumulator.push(currentValue.split(' ').filter(item => !!item.trim() && item.trim() !== "").reduce((accumulator, currentValue, index) => { + if (index > headers.length - 1) { + accumulator[headers[headers.length - 1]] = (!!accumulator[headers[headers.length - 1]].trim() && accumulator[headers[headers.length - 1]].trim() !== "") ? `${accumulator[headers[headers.length - 1]]} ${currentValue}` : currentValue; + } else { + accumulator[headers[index]] = currentValue; + } + return accumulator; + }, {})); + return accumulator; + }, []); + } catch (e) { + console.error(e); + } +}; diff --git a/benchmarks/pairs/vfc-js/recipes.toml b/benchmarks/pairs/vfc-js/recipes.toml new file mode 100644 index 0000000..40ff431 --- /dev/null +++ b/benchmarks/pairs/vfc-js/recipes.toml @@ -0,0 +1,602 @@ +# vfc-js harvest recipes derived from SecBench.js fixCommit metadata by build_recipes.py. +# Excerpts come from the upstream package repository (its license), never from SecBench.js. + +[[recipe]] +name = "angular-http-server-1-0-0" +host = "github" +repo = "simonh1000/angular-http-server" +parent = "d171a771a92e4685b24fb77cb894a7b7b8024c5c" +commit = "8bafc9577161469f5dea01e0b98ea9c525d063e9" +path = "angular-http-server.js" +mode = "hunk" +language = "javascript" +relpath = "angular-http-server.js" +license = "Apache-2.0" +cve = "" +year = 2018 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "human" +origin = "secbench.js" +project = "angular-http-server" +sink_location = "angular-http-server.js:16:29" +secbench_class = "path-traversal" +evidence = "SecBench.js path-traversal sink angular-http-server.js:16:29 fixed upstream in 8bafc9577161" +commit_url = "https://github.com/simonh1000/angular-http-server/commit/8bafc9577161469f5dea01e0b98ea9c525d063e9" +split = "train" + +[[recipe]] +name = "apex-publish-static-files-2-0-0" +host = "github" +repo = "vincentmorneau/apex-publish-static-files" +parent = "8d28fe8a9a49e5ff3f61e239293b5646528e2915" +commit = "2209af8f2b65c24aa55ab757e0e05b958c16f063" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "CVE-2018-16462" +year = 2018 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "apex-publish-static-files" +sink_location = "index.js:54:25" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink index.js:54:25 fixed upstream in 2209af8f2b65" +commit_url = "https://github.com/vincentmorneau/apex-publish-static-files/commit/2209af8f2b65c24aa55ab757e0e05b958c16f063" +split = "holdout" + +[[recipe]] +name = "arpping-2-0-0" +host = "github" +repo = "haf-decent/arpping" +parent = "ce9efe8ab7f059ddf45ae4f8f7587689ac3c068c" +commit = "0db06723f8c3c73a5cc0829c667d45cba25ca44d" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "" +year = 2021 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "arpping" +sink_location = "index.js:120:13" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink index.js:120:13 fixed upstream in 0db06723f8c3" +commit_url = "https://github.com/haf-decent/arpping/commit/0db06723f8c3c73a5cc0829c667d45cba25ca44d" +split = "train" + +[[recipe]] +name = "bestzip-2-1-6" +host = "github" +repo = "nfriedly/node-bestzip" +parent = "fcea252e52b6078eff6caf5531bd32c53fccb59f" +commit = "45d4a901478c6a8f396c8b959dd6cf8fd3f955b6" +path = "lib/bestzip.js" +mode = "hunk" +language = "javascript" +relpath = "lib/bestzip.js" +license = "MIT" +cve = "CVE-2020-7730" +year = 2020 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "bestzip" +sink_location = "lib/bestzip.js:27:27" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink lib/bestzip.js:27:27 fixed upstream in 45d4a901478c" +commit_url = "https://github.com/nfriedly/node-bestzip/commit/45d4a901478c6a8f396c8b959dd6cf8fd3f955b6" +split = "holdout" + +[[recipe]] +name = "clamscan-1-2-0" +host = "github" +repo = "kylefarris/clamscan" +parent = "d474da7b89c0d3397cd681e79c7bcc592bff144b" +commit = "5f557c970817fe8c578ec3f7ad3bcbcef4cf5538" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "CVE-2020-7613" +year = 2020 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "clamscan" +sink_location = "index.js:495:36" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink index.js:495:36 fixed upstream in 5f557c970817" +commit_url = "https://github.com/kylefarris/clamscan/commit/5f557c970817fe8c578ec3f7ad3bcbcef4cf5538" +split = "train" + +[[recipe]] +name = "codecov-3-6-4" +host = "github" +repo = "codecov/codecov-node" +parent = "5f6cc62d4f83bf3a75c973868259798814cd231e" +commit = "c0711c656686e902af2cd92d6aecc8074de4d83d" +path = "lib/codecov.js" +mode = "hunk" +language = "javascript" +relpath = "lib/codecov.js" +license = "MIT" +cve = "CVE-2020-15123" +year = 2020 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "codecov" +sink_location = "lib/codecov.js:419:7" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink lib/codecov.js:419:7 fixed upstream in c0711c656686" +commit_url = "https://github.com/codecov/codecov-node/commit/c0711c656686e902af2cd92d6aecc8074de4d83d" +split = "holdout" + +[[recipe]] +name = "crud-file-server-0-7-0" +host = "github" +repo = "omphalos/crud-file-server" +parent = "833e7fcc2bc8406b9c76836a51ae173f50c56bf0" +commit = "4fc3b404f718abb789f4ce4272c39c7a138c7a82" +path = "crud-file-server.js" +mode = "hunk" +language = "javascript" +relpath = "crud-file-server.js" +license = "Unlicense" +cve = "CVE-2018-3733" +year = 2018 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "human" +origin = "secbench.js" +project = "crud-file-server" +sink_location = "crud-file-server.js:170:14" +secbench_class = "path-traversal" +evidence = "SecBench.js path-traversal sink crud-file-server.js:170:14 fixed upstream in 4fc3b404f718" +commit_url = "https://github.com/omphalos/crud-file-server/commit/4fc3b404f718abb789f4ce4272c39c7a138c7a82" +split = "train" + +[[recipe]] +name = "diskusage-ng-0-2-6" +host = "github" +repo = "iximiuz/node-diskusage-ng" +parent = "53c505506553c83861d1bd67823d6a382d3a5a43" +commit = "48e7e093486b528f0c81ec699573e0e4a431b8d3" +path = "lib/posix.js" +mode = "hunk" +language = "javascript" +relpath = "lib/posix.js" +license = "MIT" +cve = "CVE-2020-7631" +year = 2020 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "diskusage-ng" +sink_location = "lib/posix.js:11:5" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink lib/posix.js:11:5 fixed upstream in 48e7e093486b" +commit_url = "https://github.com/iximiuz/node-diskusage-ng/commit/48e7e093486b528f0c81ec699573e0e4a431b8d3" +split = "holdout" + +[[recipe]] +name = "dns-sync-0-1-0" +host = "github" +repo = "skoranga/node-dns-sync" +parent = "b748fe77a87749d95876028eda8e759109be4c73" +commit = "d9abaae384b198db1095735ad9c1c73d7b890a0d" +path = "lib/dns-sync.js" +mode = "hunk" +language = "javascript" +relpath = "lib/dns-sync.js" +license = "MIT" +cve = "CVE-2014-9682" +year = 2014 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "dns-sync" +sink_location = "lib/dns-sync.js:21:26" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink lib/dns-sync.js:21:26 fixed upstream in d9abaae384b1" +commit_url = "https://github.com/skoranga/node-dns-sync/commit/d9abaae384b198db1095735ad9c1c73d7b890a0d" +split = "train" + +[[recipe]] +name = "get-npm-package-version-1-0-6" +host = "github" +repo = "hoperyy/get-npm-package-version" +parent = "52797864df09049ea28d65d14620774257a965b0" +commit = "40b1cf31a0607ea66f9e30a0c3af1383b52b2dec" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "ISC" +cve = "CVE-2020-7795" +year = 2021 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "get-npm-package-version" +sink_location = "index.js:16:48" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink index.js:16:48 fixed upstream in 40b1cf31a060" +commit_url = "https://github.com/hoperyy/get-npm-package-version/commit/40b1cf31a0607ea66f9e30a0c3af1383b52b2dec" +split = "holdout" + +[[recipe]] +name = "glance-3-0-0" +host = "github" +repo = "jarofghosts/glance" +parent = "7e69a5539236386e747fb240ae87ea65ed64bb2c" +commit = "8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "CVE-2018-3715" +year = 2018 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "human" +origin = "secbench.js" +project = "glance" +sink_location = "index.js:81:14" +secbench_class = "path-traversal" +evidence = "SecBench.js path-traversal sink index.js:81:14 fixed upstream in 8cfd88e44ebd" +commit_url = "https://github.com/jarofghosts/glance/commit/8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19" +split = "train" + +[[recipe]] +name = "hostr-2-0-0" +host = "github" +repo = "henrytseng/hostr" +parent = "5827ba48fd4a08c26604b97798c57d45fe1cd51e" +commit = "789a00047459fd80b6f0a9701a1378a47fb73ba8" +path = "lib/routes/statics.js" +mode = "hunk" +language = "javascript" +relpath = "lib/routes/statics.js" +license = "MIT" +cve = "CVE-2017-16029" +year = 2016 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "human" +origin = "secbench.js" +project = "hostr" +sink_location = "lib/routes/static.js:56:27" +secbench_class = "path-traversal" +evidence = "SecBench.js path-traversal sink lib/routes/static.js:56:27 fixed upstream in 789a00047459" +commit_url = "https://github.com/henrytseng/hostr/commit/789a00047459fd80b6f0a9701a1378a47fb73ba8" +split = "holdout" + +[[recipe]] +name = "hot-formula-parser-3-0-0" +host = "github" +repo = "handsontable/formula-parser" +parent = "944136096f44ff4ffd4ba0bcef30970ca4702fce" +commit = "396b089738d4bf30eb570a4fe6a188affa95cd5e" +path = "src/grammar-parser/grammar-parser.js" +mode = "hunk" +language = "javascript" +relpath = "src/grammar-parser/grammar-parser.js" +license = "MIT" +cve = "CVE-2020-6836" +year = 2019 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "hot-formula-parser" +sink_location = "lib/grammar-parser/grammar-parser.js:258:31" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink lib/grammar-parser/grammar-parser.js:258:31 fixed upstream in 396b089738d4" +commit_url = "https://github.com/handsontable/formula-parser/commit/396b089738d4bf30eb570a4fe6a188affa95cd5e" +split = "train" + +[[recipe]] +name = "is-my-json-valid-2-20-0" +host = "github" +repo = "mafintosh/is-my-json-valid" +parent = "adf40bd9f22f6c9ee7d4c612c4193a7b835416bc" +commit = "3419563687df463b4ca709a2b46be8e15d6a2b3d" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "" +year = 2020 +cwe = "CWE-94" +class = "code injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "is-my-json-valid" +sink_location = "index.js:172:21" +secbench_class = "code-injection" +evidence = "SecBench.js code-injection sink index.js:172:21 fixed upstream in 3419563687df" +commit_url = "https://github.com/mafintosh/is-my-json-valid/commit/3419563687df463b4ca709a2b46be8e15d6a2b3d" +split = "holdout" + +[[recipe]] +name = "libnmap-0-4-11" +host = "github" +repo = "jas-/node-libnmap" +parent = "49b72239b144ab7ffdf673b62e18bfd0760948f6" +commit = "c235b24b0342e12a5dbf502f89daabe95883b6c8" +path = "lib/classes/tools.js" +mode = "hunk" +language = "javascript" +relpath = "lib/classes/tools.js" +license = "MIT" +cve = "CVE-2018-16461" +year = 2018 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "libnmap" +sink_location = "lib/classes/tools.js:114:25" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink lib/classes/tools.js:114:25 fixed upstream in c235b24b0342" +commit_url = "https://github.com/jas-/node-libnmap/commit/c235b24b0342e12a5dbf502f89daabe95883b6c8" +split = "train" + +[[recipe]] +name = "node-simple-router-0-10-0" +host = "github" +repo = "sandy98/node-simple-router" +parent = "91da4299aee0086cc1e0cc837763578af6f82cea" +commit = "dfdd52e2e80607af433097d940b3834fd96df488" +path = "lib/router.js" +mode = "hunk" +language = "javascript" +relpath = "lib/router.js" +license = "MIT" +cve = "CVE-2017-16083" +year = 2017 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "human" +origin = "secbench.js" +project = "node-simple-router" +sink_location = "lib/router.js:757:27" +secbench_class = "path-traversal" +evidence = "SecBench.js path-traversal sink lib/router.js:757:27 fixed upstream in dfdd52e2e806" +commit_url = "https://github.com/sandy98/node-simple-router/commit/dfdd52e2e80607af433097d940b3834fd96df488" +split = "holdout" + +[[recipe]] +name = "pdf-image-1-0-5" +host = "github" +repo = "roest01/node-pdf-image" +parent = "d8c0dcadd93985dbefe80b84060f5f572bfbb6ff" +commit = "54679496a89738443917608c2bbe2f6e5dd20e83" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "CVE-2018-3757" +year = 2018 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "pdf-image" +sink_location = "index.js:43:7" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink index.js:43:7 fixed upstream in 54679496a897" +commit_url = "https://github.com/roest01/node-pdf-image/commit/54679496a89738443917608c2bbe2f6e5dd20e83" +split = "train" + +[[recipe]] +name = "pidusage-1-0-0" +host = "github" +repo = "soyuka/pidusage" +parent = "e3660d8a8e5e61afad9b02af67356a1e239d6bde" +commit = "b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d" +path = "lib/stats.js" +mode = "hunk" +language = "javascript" +relpath = "lib/stats.js" +license = "MIT" +cve = "CVE-2017-16034" +year = 2017 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "pidusage" +sink_location = "lib/stats.js:108:5" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink lib/stats.js:108:5 fixed upstream in b70eca15f7ca" +commit_url = "https://github.com/soyuka/pidusage/commit/b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d" +split = "holdout" + +[[recipe]] +name = "portprocesses-1-0-4" +host = "github" +repo = "rrainn/PortProcesses" +parent = "fffceb09aff7180afbd0bd172e820404b33c8299" +commit = "86811216c9b97b01b5722f879f8c88a7aa4214e1" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "CVE-2021-23348" +year = 2021 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "portprocesses" +sink_location = "exec.js:5:3" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink exec.js:5:3 fixed upstream in 86811216c9b9" +commit_url = "https://github.com/rrainn/PortProcesses/commit/86811216c9b97b01b5722f879f8c88a7aa4214e1" +split = "train" + +[[recipe]] +name = "serve-here-js-1-1-0" +host = "github" +repo = "ChristoPy/serve-here.js" +parent = "ad077637bf7a9fe0dee3262f011aed19a45ce92c" +commit = "cefb51d03290b6a88dd13143ab2de31b8cf57c39" +path = "source/response.js" +mode = "hunk" +language = "javascript" +relpath = "source/response.js" +license = "MIT" +cve = "CVE-2019-5444" +year = 2019 +cwe = "CWE-22" +class = "path traversal" +mechanism = "path_join_user_input" +provenance = "human" +origin = "secbench.js" +project = "serve-here.js" +sink_location = "source/server.js:27:15" +secbench_class = "path-traversal" +evidence = "SecBench.js path-traversal sink source/server.js:27:15 fixed upstream in cefb51d03290" +commit_url = "https://github.com/ChristoPy/serve-here.js/commit/cefb51d03290b6a88dd13143ab2de31b8cf57c39" +split = "holdout" + +[[recipe]] +name = "static-eval-1-1-1" +host = "github" +repo = "browserify/static-eval" +parent = "5bba7905b0e797460b8f6c2e38ff6869146faab3" +commit = "32f8efd072625e267aa7237d2daff35738c50d2a" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "" +year = 2019 +cwe = "CWE-94" +class = "code injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "static-eval" +sink_location = "index.js:118:20" +secbench_class = "code-injection" +evidence = "SecBench.js code-injection sink index.js:118:20 fixed upstream in 32f8efd07262" +commit_url = "https://github.com/browserify/static-eval/commit/32f8efd072625e267aa7237d2daff35738c50d2a" +split = "train" + +[[recipe]] +name = "thenify-3-3-0" +host = "github" +repo = "thenables/thenify" +parent = "aa21cfb64f47238c764d258d0faf7366a8487e36" +commit = "0d94a24eb933bc835d568f3009f4d269c4c4c17a" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "CVE-2020-7677" +year = 2020 +cwe = "CWE-94" +class = "code injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "thenify" +sink_location = "index.js:17:10" +secbench_class = "code-injection" +evidence = "SecBench.js code-injection sink index.js:17:10 fixed upstream in 0d94a24eb933" +commit_url = "https://github.com/thenables/thenify/commit/0d94a24eb933bc835d568f3009f4d269c4c4c17a" +split = "holdout" + +[[recipe]] +name = "underscore-1-13-0-0" +host = "github" +repo = "jashkenas/underscore" +parent = "ef646ccbbbd971e82719c83fc69f56a5585b3a01" +commit = "4c73526d43838ad6ab43a6134728776632adeb66" +path = "modules/template.js" +mode = "hunk" +language = "javascript" +relpath = "modules/template.js" +license = "MIT" +cve = "CVE-2021-23358" +year = 2021 +cwe = "CWE-94" +class = "code injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "underscore" +sink_location = "modules/template.js:71:14" +secbench_class = "code-injection" +evidence = "SecBench.js code-injection sink modules/template.js:71:14 fixed upstream in 4c73526d4383" +commit_url = "https://github.com/jashkenas/underscore/commit/4c73526d43838ad6ab43a6134728776632adeb66" +split = "train" + +[[recipe]] +name = "whereis-0-4-0" +host = "github" +repo = "vvo/node-whereis" +parent = "b8b642b1163baa37f15ed49b4064dcbdb6e1876c" +commit = "0f64e3780235004fb6e43bfd153ea3e0e210ee2b" +path = "index.js" +mode = "hunk" +language = "javascript" +relpath = "index.js" +license = "MIT" +cve = "CVE-2018-3772" +year = 2018 +cwe = "CWE-78" +class = "command injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "secbench.js" +project = "whereis" +sink_location = "index.js:4:6" +secbench_class = "command-injection" +evidence = "SecBench.js command-injection sink index.js:4:6 fixed upstream in 0f64e3780235" +commit_url = "https://github.com/vvo/node-whereis/commit/0f64e3780235004fb6e43bfd153ea3e0e210ee2b" +split = "holdout" diff --git a/benchmarks/pairs/vfc-js/serve-here-js-1-1-0/fixed.js b/benchmarks/pairs/vfc-js/serve-here-js-1-1-0/fixed.js new file mode 100644 index 0000000..d2b7412 --- /dev/null +++ b/benchmarks/pairs/vfc-js/serve-here-js-1-1-0/fixed.js @@ -0,0 +1,29 @@ +// Provenance: ChristoPy/serve-here.js (fixed). +// repo: ChristoPy/serve-here.js +// commit: cefb51d03290b6a88dd13143ab2de31b8cf57c39 +// parent: ad077637bf7a9fe0dee3262f011aed19a45ce92c +// commit_url: https://github.com/ChristoPy/serve-here.js/commit/cefb51d03290b6a88dd13143ab2de31b8cf57c39 +// cve: CVE-2019-5444 +// license: MIT +// function: ConfigureFilePath +// relpath: source/response.js +// provenance: human +// mechanism: path_join_user_input +// upstream_start: 14 + +module.exports.ConfigureFilePath = (Options, FilePath) => { + + const Slash = FilePath.split ("")[FilePath.split ("").length - 1] === "/"; + + if (FilePath === "/") { + return `${Options.RootFolder}/${Options.IndexFile}`; + } else { + const IsMalicious = HasMaliciousPath(Options, FilePath); + + if (IsMalicious) { + return null; + } else { + return (Slash ? `${Options.RootFolder}${FilePath.slice (0, -1)}` : `${Options.RootFolder}${FilePath}`); + } + } +} diff --git a/benchmarks/pairs/vfc-js/serve-here-js-1-1-0/vuln.js b/benchmarks/pairs/vfc-js/serve-here-js-1-1-0/vuln.js new file mode 100644 index 0000000..4828f0f --- /dev/null +++ b/benchmarks/pairs/vfc-js/serve-here-js-1-1-0/vuln.js @@ -0,0 +1,23 @@ +// Provenance: ChristoPy/serve-here.js (vuln). +// repo: ChristoPy/serve-here.js +// commit: ad077637bf7a9fe0dee3262f011aed19a45ce92c +// parent: ad077637bf7a9fe0dee3262f011aed19a45ce92c +// commit_url: https://github.com/ChristoPy/serve-here.js/commit/cefb51d03290b6a88dd13143ab2de31b8cf57c39 +// cve: CVE-2019-5444 +// license: MIT +// function: ConfigureFilePath +// relpath: source/response.js +// provenance: human +// mechanism: path_join_user_input +// upstream_start: 5 + +module.exports.ConfigureFilePath = (Options, FilePath) => { + + const Slash = FilePath.split ("")[FilePath.split ("").length - 1] === "/"; + + switch (FilePath) { + + case "/": return `${Options.RootFolder}/${Options.IndexFile}`; + default: return (Slash ? `${Options.RootFolder}${FilePath.slice (0, -1)}` : `${Options.RootFolder}${FilePath}`); + } +} diff --git a/benchmarks/pairs/vfc-js/static-eval-1-1-1/fixed.js b/benchmarks/pairs/vfc-js/static-eval-1-1-1/fixed.js new file mode 100644 index 0000000..5feabc7 --- /dev/null +++ b/benchmarks/pairs/vfc-js/static-eval-1-1-1/fixed.js @@ -0,0 +1,188 @@ +// Provenance: browserify/static-eval (fixed). +// repo: browserify/static-eval +// commit: 32f8efd072625e267aa7237d2daff35738c50d2a +// parent: 5bba7905b0e797460b8f6c2e38ff6869146faab3 +// commit_url: https://github.com/browserify/static-eval/commit/32f8efd072625e267aa7237d2daff35738c50d2a +// cve: +// license: MIT +// function: walk +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 3 + +module.exports = function (ast, vars) { + if (!vars) vars = {}; + var FAIL = {}; + + var result = (function walk (node, scopeVars) { + if (node.type === 'Literal') { + return node.value; + } + else if (node.type === 'UnaryExpression'){ + var val = walk(node.argument) + if (node.operator === '+') return +val + if (node.operator === '-') return -val + if (node.operator === '~') return ~val + if (node.operator === '!') return !val + return FAIL + } + else if (node.type === 'ArrayExpression') { + var xs = []; + for (var i = 0, l = node.elements.length; i < l; i++) { + var x = walk(node.elements[i]); + if (x === FAIL) return FAIL; + xs.push(x); + } + return xs; + } + else if (node.type === 'ObjectExpression') { + var obj = {}; + for (var i = 0; i < node.properties.length; i++) { + var prop = node.properties[i]; + var value = prop.value === null + ? prop.value + : walk(prop.value) + ; + if (value === FAIL) return FAIL; + obj[prop.key.value || prop.key.name] = value; + } + return obj; + } + else if (node.type === 'BinaryExpression' || + node.type === 'LogicalExpression') { + var l = walk(node.left); + if (l === FAIL) return FAIL; + var r = walk(node.right); + if (r === FAIL) return FAIL; + + var op = node.operator; + if (op === '==') return l == r; + if (op === '===') return l === r; + if (op === '!=') return l != r; + if (op === '!==') return l !== r; + if (op === '+') return l + r; + if (op === '-') return l - r; + if (op === '*') return l * r; + if (op === '/') return l / r; + if (op === '%') return l % r; + if (op === '<') return l < r; + if (op === '<=') return l <= r; + if (op === '>') return l > r; + if (op === '>=') return l >= r; + if (op === '|') return l | r; + if (op === '&') return l & r; + if (op === '^') return l ^ r; + if (op === '&&') return l && r; + if (op === '||') return l || r; + + return FAIL; + } + else if (node.type === 'Identifier') { + if ({}.hasOwnProperty.call(vars, node.name)) { + return vars[node.name]; + } + else return FAIL; + } + else if (node.type === 'ThisExpression') { + if ({}.hasOwnProperty.call(vars, 'this')) { + return vars['this']; + } + else return FAIL; + } + else if (node.type === 'CallExpression') { + var callee = walk(node.callee); + if (callee === FAIL) return FAIL; + if (typeof callee !== 'function') return FAIL; + + var ctx = node.callee.object ? walk(node.callee.object) : FAIL; + if (ctx === FAIL) ctx = null; + + var args = []; + for (var i = 0, l = node.arguments.length; i < l; i++) { + var x = walk(node.arguments[i]); + if (x === FAIL) return FAIL; + args.push(x); + } + return callee.apply(ctx, args); + } + else if (node.type === 'MemberExpression') { + var obj = walk(node.object); + // do not allow access to methods on Function + if((obj === FAIL) || (typeof obj == 'function')){ + return FAIL; + } + if (node.property.type === 'Identifier') { + return obj[node.property.name]; + } + var prop = walk(node.property); + if (prop === FAIL) return FAIL; + return obj[prop]; + } + else if (node.type === 'ConditionalExpression') { + var val = walk(node.test) + if (val === FAIL) return FAIL; + return val ? walk(node.consequent) : walk(node.alternate) + } + else if (node.type === 'ExpressionStatement') { + var val = walk(node.expression) + if (val === FAIL) return FAIL; + return val; + } + else if (node.type === 'ReturnStatement') { + return walk(node.argument) + } + else if (node.type === 'FunctionExpression') { + + var bodies = node.body.body; + + // Create a "scope" for our arguments + var oldVars = {}; + Object.keys(vars).forEach(function(element){ + oldVars[element] = vars[element]; + }) + + node.params.forEach(function(key) { + if(key.type == 'Identifier'){ + vars[key.name] = null; + } + else return FAIL; + }); + for(var i in bodies){ + if(walk(bodies[i]) === FAIL){ + return FAIL; + } + } + // restore the vars and scope after we walk + vars = oldVars; + + var keys = Object.keys(vars); + var vals = keys.map(function(key) { + return vars[key]; + }); + return Function(keys.join(', '), 'return ' + unparse(node)).apply(null, vals); + } + else if (node.type === 'TemplateLiteral') { + var str = ''; + for (var i = 0; i < node.expressions.length; i++) { + str += walk(node.quasis[i]); + str += walk(node.expressions[i]); + } + str += walk(node.quasis[i]); + return str; + } + else if (node.type === 'TaggedTemplateExpression') { + var tag = walk(node.tag); + var quasi = node.quasi; + var strings = quasi.quasis.map(walk); + var values = quasi.expressions.map(walk); + return tag.apply(null, [strings].concat(values)); + } + else if (node.type === 'TemplateElement') { + return node.value.cooked; + } + else return FAIL; + })(ast); + + return result === FAIL ? undefined : result; +}; diff --git a/benchmarks/pairs/vfc-js/static-eval-1-1-1/vuln.js b/benchmarks/pairs/vfc-js/static-eval-1-1-1/vuln.js new file mode 100644 index 0000000..dbdc955 --- /dev/null +++ b/benchmarks/pairs/vfc-js/static-eval-1-1-1/vuln.js @@ -0,0 +1,187 @@ +// Provenance: browserify/static-eval (vuln). +// repo: browserify/static-eval +// commit: 5bba7905b0e797460b8f6c2e38ff6869146faab3 +// parent: 5bba7905b0e797460b8f6c2e38ff6869146faab3 +// commit_url: https://github.com/browserify/static-eval/commit/32f8efd072625e267aa7237d2daff35738c50d2a +// cve: +// license: MIT +// function: walk +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 3 + +module.exports = function (ast, vars) { + if (!vars) vars = {}; + var FAIL = {}; + + var result = (function walk (node, scopeVars) { + if (node.type === 'Literal') { + return node.value; + } + else if (node.type === 'UnaryExpression'){ + var val = walk(node.argument) + if (node.operator === '+') return +val + if (node.operator === '-') return -val + if (node.operator === '~') return ~val + if (node.operator === '!') return !val + return FAIL + } + else if (node.type === 'ArrayExpression') { + var xs = []; + for (var i = 0, l = node.elements.length; i < l; i++) { + var x = walk(node.elements[i]); + if (x === FAIL) return FAIL; + xs.push(x); + } + return xs; + } + else if (node.type === 'ObjectExpression') { + var obj = {}; + for (var i = 0; i < node.properties.length; i++) { + var prop = node.properties[i]; + var value = prop.value === null + ? prop.value + : walk(prop.value) + ; + if (value === FAIL) return FAIL; + obj[prop.key.value || prop.key.name] = value; + } + return obj; + } + else if (node.type === 'BinaryExpression' || + node.type === 'LogicalExpression') { + var l = walk(node.left); + if (l === FAIL) return FAIL; + var r = walk(node.right); + if (r === FAIL) return FAIL; + + var op = node.operator; + if (op === '==') return l == r; + if (op === '===') return l === r; + if (op === '!=') return l != r; + if (op === '!==') return l !== r; + if (op === '+') return l + r; + if (op === '-') return l - r; + if (op === '*') return l * r; + if (op === '/') return l / r; + if (op === '%') return l % r; + if (op === '<') return l < r; + if (op === '<=') return l <= r; + if (op === '>') return l > r; + if (op === '>=') return l >= r; + if (op === '|') return l | r; + if (op === '&') return l & r; + if (op === '^') return l ^ r; + if (op === '&&') return l && r; + if (op === '||') return l || r; + + return FAIL; + } + else if (node.type === 'Identifier') { + if ({}.hasOwnProperty.call(vars, node.name)) { + return vars[node.name]; + } + else return FAIL; + } + else if (node.type === 'ThisExpression') { + if ({}.hasOwnProperty.call(vars, 'this')) { + return vars['this']; + } + else return FAIL; + } + else if (node.type === 'CallExpression') { + var callee = walk(node.callee); + if (callee === FAIL) return FAIL; + if (typeof callee !== 'function') return FAIL; + + var ctx = node.callee.object ? walk(node.callee.object) : FAIL; + if (ctx === FAIL) ctx = null; + + var args = []; + for (var i = 0, l = node.arguments.length; i < l; i++) { + var x = walk(node.arguments[i]); + if (x === FAIL) return FAIL; + args.push(x); + } + return callee.apply(ctx, args); + } + else if (node.type === 'MemberExpression') { + var obj = walk(node.object); + // do not allow access to methods on Function + if((obj === FAIL) || (typeof obj == 'function')){ + return FAIL; + } + if (node.property.type === 'Identifier') { + return obj[node.property.name]; + } + var prop = walk(node.property); + if (prop === FAIL) return FAIL; + return obj[prop]; + } + else if (node.type === 'ConditionalExpression') { + var val = walk(node.test) + if (val === FAIL) return FAIL; + return val ? walk(node.consequent) : walk(node.alternate) + } + else if (node.type === 'ExpressionStatement') { + var val = walk(node.expression) + if (val === FAIL) return FAIL; + return val; + } + else if (node.type === 'ReturnStatement') { + return walk(node.argument) + } + else if (node.type === 'FunctionExpression') { + + var bodies = node.body.body; + + // Create a "scope" for our arguments + var oldVars = {}; + Object.keys(vars).forEach(function(element){ + oldVars[element] = vars[element]; + }) + + node.params.forEach(function(key) { + if(key.type == 'Identifier'){ + vars[key.name] = null; + } + }); + for(var i in bodies){ + if(walk(bodies[i]) === FAIL){ + return FAIL; + } + } + // restore the vars and scope after we walk + vars = oldVars; + + var keys = Object.keys(vars); + var vals = keys.map(function(key) { + return vars[key]; + }); + return Function(keys.join(', '), 'return ' + unparse(node)).apply(null, vals); + } + else if (node.type === 'TemplateLiteral') { + var str = ''; + for (var i = 0; i < node.expressions.length; i++) { + str += walk(node.quasis[i]); + str += walk(node.expressions[i]); + } + str += walk(node.quasis[i]); + return str; + } + else if (node.type === 'TaggedTemplateExpression') { + var tag = walk(node.tag); + var quasi = node.quasi; + var strings = quasi.quasis.map(walk); + var values = quasi.expressions.map(walk); + return tag.apply(null, [strings].concat(values)); + } + else if (node.type === 'TemplateElement') { + return node.value.cooked; + } + else return FAIL; + })(ast); + + return result === FAIL ? undefined : result; +}; diff --git a/benchmarks/pairs/vfc-js/thenify-3-3-0/fixed.js b/benchmarks/pairs/vfc-js/thenify-3-3-0/fixed.js new file mode 100644 index 0000000..4bd4d67 --- /dev/null +++ b/benchmarks/pairs/vfc-js/thenify-3-3-0/fixed.js @@ -0,0 +1,30 @@ +// Provenance: thenables/thenify (fixed). +// repo: thenables/thenify +// commit: 0d94a24eb933bc835d568f3009f4d269c4c4c17a +// parent: aa21cfb64f47238c764d258d0faf7366a8487e36 +// commit_url: https://github.com/thenables/thenify/commit/0d94a24eb933bc835d568f3009f4d269c4c4c17a +// cve: CVE-2020-7677 +// license: MIT +// function: thenify +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 2 + +var Promise = require('any-promise') +var assert = require('assert') + +module.exports = thenify + +/** + * Turn async functions into promises + * + * @param {Function} fn + * @return {Function} + * @api public + */ + +function thenify(fn, options) { + assert(typeof fn === 'function') + return createWrapper(fn, options) +} diff --git a/benchmarks/pairs/vfc-js/thenify-3-3-0/vuln.js b/benchmarks/pairs/vfc-js/thenify-3-3-0/vuln.js new file mode 100644 index 0000000..4f838d9 --- /dev/null +++ b/benchmarks/pairs/vfc-js/thenify-3-3-0/vuln.js @@ -0,0 +1,30 @@ +// Provenance: thenables/thenify (vuln). +// repo: thenables/thenify +// commit: aa21cfb64f47238c764d258d0faf7366a8487e36 +// parent: aa21cfb64f47238c764d258d0faf7366a8487e36 +// commit_url: https://github.com/thenables/thenify/commit/0d94a24eb933bc835d568f3009f4d269c4c4c17a +// cve: CVE-2020-7677 +// license: MIT +// function: thenify +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 2 + +var Promise = require('any-promise') +var assert = require('assert') + +module.exports = thenify + +/** + * Turn async functions into promises + * + * @param {Function} $$__fn__$$ + * @return {Function} + * @api public + */ + +function thenify($$__fn__$$, options) { + assert(typeof $$__fn__$$ === 'function') + return eval(createWrapper($$__fn__$$.name, options)) +} diff --git a/benchmarks/pairs/vfc-js/training/manifest.jsonl b/benchmarks/pairs/vfc-js/training/manifest.jsonl new file mode 100644 index 0000000..8118d91 --- /dev/null +++ b/benchmarks/pairs/vfc-js/training/manifest.jsonl @@ -0,0 +1,34 @@ +{"class": "path traversal", "commit": "d171a771a92e4685b24fb77cb894a7b7b8024c5c", "commit_url": "https://github.com/simonh1000/angular-http-server/commit/8bafc9577161469f5dea01e0b98ea9c525d063e9", "cve": "", "cwe": "CWE-22", "file": "angular-http-server-1-0-0/vuln.js", "function": "requestListener", "id": "angular-http-server-1-0-0:vuln", "label": "vuln", "license": "Apache-2.0", "mechanism": "path_join_user_input", "pair": "angular-http-server-1-0-0", "project": "angular-http-server", "provenance": "human", "relpath": "angular-http-server.js", "repo": "simonh1000/angular-http-server", "split": "train", "year": 2018} +{"class": "path traversal", "commit": "8bafc9577161469f5dea01e0b98ea9c525d063e9", "commit_url": "https://github.com/simonh1000/angular-http-server/commit/8bafc9577161469f5dea01e0b98ea9c525d063e9", "cve": "", "cwe": "CWE-22", "file": "angular-http-server-1-0-0/fixed.js", "function": "requestListener", "id": "angular-http-server-1-0-0:fixed", "label": "fixed", "license": "Apache-2.0", "mechanism": "path_join_user_input", "pair": "angular-http-server-1-0-0", "project": "angular-http-server", "provenance": "human", "relpath": "angular-http-server.js", "repo": "simonh1000/angular-http-server", "split": "train", "year": 2018} +{"class": "command injection", "commit": "8d28fe8a9a49e5ff3f61e239293b5646528e2915", "commit_url": "https://github.com/vincentmorneau/apex-publish-static-files/commit/2209af8f2b65c24aa55ab757e0e05b958c16f063", "cve": "CVE-2018-16462", "cwe": "CWE-78", "file": "apex-publish-static-files-2-0-0/vuln.js", "function": "publish", "id": "apex-publish-static-files-2-0-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "apex-publish-static-files-2-0-0", "project": "apex-publish-static-files", "provenance": "human", "relpath": "index.js", "repo": "vincentmorneau/apex-publish-static-files", "split": "holdout", "year": 2018} +{"class": "command injection", "commit": "2209af8f2b65c24aa55ab757e0e05b958c16f063", "commit_url": "https://github.com/vincentmorneau/apex-publish-static-files/commit/2209af8f2b65c24aa55ab757e0e05b958c16f063", "cve": "CVE-2018-16462", "cwe": "CWE-78", "file": "apex-publish-static-files-2-0-0/fixed.js", "function": "publish", "id": "apex-publish-static-files-2-0-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "apex-publish-static-files-2-0-0", "project": "apex-publish-static-files", "provenance": "human", "relpath": "index.js", "repo": "vincentmorneau/apex-publish-static-files", "split": "holdout", "year": 2018} +{"class": "command injection", "commit": "fcea252e52b6078eff6caf5531bd32c53fccb59f", "commit_url": "https://github.com/nfriedly/node-bestzip/commit/45d4a901478c6a8f396c8b959dd6cf8fd3f955b6", "cve": "CVE-2020-7730", "cwe": "CWE-78", "file": "bestzip-2-1-6/vuln.js", "function": "nativeZip", "id": "bestzip-2-1-6:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "bestzip-2-1-6", "project": "bestzip", "provenance": "human", "relpath": "lib/bestzip.js", "repo": "nfriedly/node-bestzip", "split": "holdout", "year": 2020} +{"class": "command injection", "commit": "45d4a901478c6a8f396c8b959dd6cf8fd3f955b6", "commit_url": "https://github.com/nfriedly/node-bestzip/commit/45d4a901478c6a8f396c8b959dd6cf8fd3f955b6", "cve": "CVE-2020-7730", "cwe": "CWE-78", "file": "bestzip-2-1-6/fixed.js", "function": "nativeZip", "id": "bestzip-2-1-6:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "bestzip-2-1-6", "project": "bestzip", "provenance": "human", "relpath": "lib/bestzip.js", "repo": "nfriedly/node-bestzip", "split": "holdout", "year": 2020} +{"class": "command injection", "commit": "d474da7b89c0d3397cd681e79c7bcc592bff144b", "commit_url": "https://github.com/kylefarris/clamscan/commit/5f557c970817fe8c578ec3f7ad3bcbcef4cf5538", "cve": "CVE-2020-7613", "cwe": "CWE-78", "file": "clamscan-1-2-0/vuln.js", "function": "local_scan", "id": "clamscan-1-2-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "clamscan-1-2-0", "project": "clamscan", "provenance": "human", "relpath": "index.js", "repo": "kylefarris/clamscan", "split": "train", "year": 2020} +{"class": "command injection", "commit": "5f557c970817fe8c578ec3f7ad3bcbcef4cf5538", "commit_url": "https://github.com/kylefarris/clamscan/commit/5f557c970817fe8c578ec3f7ad3bcbcef4cf5538", "cve": "CVE-2020-7613", "cwe": "CWE-78", "file": "clamscan-1-2-0/fixed.js", "function": "local_scan", "id": "clamscan-1-2-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "clamscan-1-2-0", "project": "clamscan", "provenance": "human", "relpath": "index.js", "repo": "kylefarris/clamscan", "split": "train", "year": 2020} +{"class": "command injection", "commit": "53c505506553c83861d1bd67823d6a382d3a5a43", "commit_url": "https://github.com/iximiuz/node-diskusage-ng/commit/48e7e093486b528f0c81ec699573e0e4a431b8d3", "cve": "CVE-2020-7631", "cwe": "CWE-78", "file": "diskusage-ng-0-2-6/vuln.js", "function": "diskusage", "id": "diskusage-ng-0-2-6:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "diskusage-ng-0-2-6", "project": "diskusage-ng", "provenance": "human", "relpath": "lib/posix.js", "repo": "iximiuz/node-diskusage-ng", "split": "holdout", "year": 2020} +{"class": "command injection", "commit": "48e7e093486b528f0c81ec699573e0e4a431b8d3", "commit_url": "https://github.com/iximiuz/node-diskusage-ng/commit/48e7e093486b528f0c81ec699573e0e4a431b8d3", "cve": "CVE-2020-7631", "cwe": "CWE-78", "file": "diskusage-ng-0-2-6/fixed.js", "function": "diskusage", "id": "diskusage-ng-0-2-6:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "diskusage-ng-0-2-6", "project": "diskusage-ng", "provenance": "human", "relpath": "lib/posix.js", "repo": "iximiuz/node-diskusage-ng", "split": "holdout", "year": 2020} +{"class": "command injection", "commit": "b748fe77a87749d95876028eda8e759109be4c73", "commit_url": "https://github.com/skoranga/node-dns-sync/commit/d9abaae384b198db1095735ad9c1c73d7b890a0d", "cve": "CVE-2014-9682", "cwe": "CWE-78", "file": "dns-sync-0-1-0/vuln.js", "function": "resolve", "id": "dns-sync-0-1-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dns-sync-0-1-0", "project": "dns-sync", "provenance": "human", "relpath": "lib/dns-sync.js", "repo": "skoranga/node-dns-sync", "split": "train", "year": 2014} +{"class": "command injection", "commit": "d9abaae384b198db1095735ad9c1c73d7b890a0d", "commit_url": "https://github.com/skoranga/node-dns-sync/commit/d9abaae384b198db1095735ad9c1c73d7b890a0d", "cve": "CVE-2014-9682", "cwe": "CWE-78", "file": "dns-sync-0-1-0/fixed.js", "function": "resolve", "id": "dns-sync-0-1-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dns-sync-0-1-0", "project": "dns-sync", "provenance": "human", "relpath": "lib/dns-sync.js", "repo": "skoranga/node-dns-sync", "split": "train", "year": 2014} +{"class": "path traversal", "commit": "7e69a5539236386e747fb240ae87ea65ed64bb2c", "commit_url": "https://github.com/jarofghosts/glance/commit/8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19", "cve": "CVE-2018-3715", "cwe": "CWE-22", "file": "glance-3-0-0/vuln.js", "function": "Glance$serveRequest", "id": "glance-3-0-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "path_join_user_input", "pair": "glance-3-0-0", "project": "glance", "provenance": "human", "relpath": "index.js", "repo": "jarofghosts/glance", "split": "train", "year": 2018} +{"class": "path traversal", "commit": "8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19", "commit_url": "https://github.com/jarofghosts/glance/commit/8cfd88e44ebd3f07e3a2eaf376a3e758b6c4ca19", "cve": "CVE-2018-3715", "cwe": "CWE-22", "file": "glance-3-0-0/fixed.js", "function": "Glance$serveRequest", "id": "glance-3-0-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "path_join_user_input", "pair": "glance-3-0-0", "project": "glance", "provenance": "human", "relpath": "index.js", "repo": "jarofghosts/glance", "split": "train", "year": 2018} +{"class": "command injection", "commit": "944136096f44ff4ffd4ba0bcef30970ca4702fce", "commit_url": "https://github.com/handsontable/formula-parser/commit/396b089738d4bf30eb570a4fe6a188affa95cd5e", "cve": "CVE-2020-6836", "cwe": "CWE-78", "file": "hot-formula-parser-3-0-0/vuln.js", "function": "performAction", "id": "hot-formula-parser-3-0-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "hot-formula-parser-3-0-0", "project": "hot-formula-parser", "provenance": "human", "relpath": "src/grammar-parser/grammar-parser.js", "repo": "handsontable/formula-parser", "split": "train", "year": 2019} +{"class": "command injection", "commit": "396b089738d4bf30eb570a4fe6a188affa95cd5e", "commit_url": "https://github.com/handsontable/formula-parser/commit/396b089738d4bf30eb570a4fe6a188affa95cd5e", "cve": "CVE-2020-6836", "cwe": "CWE-78", "file": "hot-formula-parser-3-0-0/fixed.js", "function": "performAction", "id": "hot-formula-parser-3-0-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "hot-formula-parser-3-0-0", "project": "hot-formula-parser", "provenance": "human", "relpath": "src/grammar-parser/grammar-parser.js", "repo": "handsontable/formula-parser", "split": "train", "year": 2019} +{"class": "command injection", "commit": "49b72239b144ab7ffdf673b62e18bfd0760948f6", "commit_url": "https://github.com/jas-/node-libnmap/commit/c235b24b0342e12a5dbf502f89daabe95883b6c8", "cve": "CVE-2018-16461", "cwe": "CWE-78", "file": "libnmap-0-4-11/vuln.js", "function": "block", "id": "libnmap-0-4-11:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "libnmap-0-4-11", "project": "libnmap", "provenance": "human", "relpath": "lib/classes/tools.js", "repo": "jas-/node-libnmap", "split": "train", "year": 2018} +{"class": "command injection", "commit": "c235b24b0342e12a5dbf502f89daabe95883b6c8", "commit_url": "https://github.com/jas-/node-libnmap/commit/c235b24b0342e12a5dbf502f89daabe95883b6c8", "cve": "CVE-2018-16461", "cwe": "CWE-78", "file": "libnmap-0-4-11/fixed.js", "function": "block", "id": "libnmap-0-4-11:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "libnmap-0-4-11", "project": "libnmap", "provenance": "human", "relpath": "lib/classes/tools.js", "repo": "jas-/node-libnmap", "split": "train", "year": 2018} +{"class": "command injection", "commit": "d8c0dcadd93985dbefe80b84060f5f572bfbb6ff", "commit_url": "https://github.com/roest01/node-pdf-image/commit/54679496a89738443917608c2bbe2f6e5dd20e83", "cve": "CVE-2018-3757", "cwe": "CWE-78", "file": "pdf-image-1-0-5/vuln.js", "function": "getInfo", "id": "pdf-image-1-0-5:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "pdf-image-1-0-5", "project": "pdf-image", "provenance": "human", "relpath": "index.js", "repo": "roest01/node-pdf-image", "split": "train", "year": 2018} +{"class": "command injection", "commit": "54679496a89738443917608c2bbe2f6e5dd20e83", "commit_url": "https://github.com/roest01/node-pdf-image/commit/54679496a89738443917608c2bbe2f6e5dd20e83", "cve": "CVE-2018-3757", "cwe": "CWE-78", "file": "pdf-image-1-0-5/fixed.js", "function": "getInfo", "id": "pdf-image-1-0-5:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "pdf-image-1-0-5", "project": "pdf-image", "provenance": "human", "relpath": "index.js", "repo": "roest01/node-pdf-image", "split": "train", "year": 2018} +{"class": "command injection", "commit": "e3660d8a8e5e61afad9b02af67356a1e239d6bde", "commit_url": "https://github.com/soyuka/pidusage/commit/b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d", "cve": "CVE-2017-16034", "cwe": "CWE-78", "file": "pidusage-1-0-0/vuln.js", "function": "ps", "id": "pidusage-1-0-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "pidusage-1-0-0", "project": "pidusage", "provenance": "human", "relpath": "lib/stats.js", "repo": "soyuka/pidusage", "split": "holdout", "year": 2017} +{"class": "command injection", "commit": "b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d", "commit_url": "https://github.com/soyuka/pidusage/commit/b70eca15f7ca7f1b82a15f8a5d4bb48737f5a89d", "cve": "CVE-2017-16034", "cwe": "CWE-78", "file": "pidusage-1-0-0/fixed.js", "function": "ps", "id": "pidusage-1-0-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "pidusage-1-0-0", "project": "pidusage", "provenance": "human", "relpath": "lib/stats.js", "repo": "soyuka/pidusage", "split": "holdout", "year": 2017} +{"class": "command injection", "commit": "fffceb09aff7180afbd0bd172e820404b33c8299", "commit_url": "https://github.com/rrainn/PortProcesses/commit/86811216c9b97b01b5722f879f8c88a7aa4214e1", "cve": "CVE-2021-23348", "cwe": "CWE-78", "file": "portprocesses-1-0-4/vuln.js", "function": "listProcessesOnPort", "id": "portprocesses-1-0-4:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "portprocesses-1-0-4", "project": "portprocesses", "provenance": "human", "relpath": "index.js", "repo": "rrainn/PortProcesses", "split": "train", "year": 2021} +{"class": "command injection", "commit": "86811216c9b97b01b5722f879f8c88a7aa4214e1", "commit_url": "https://github.com/rrainn/PortProcesses/commit/86811216c9b97b01b5722f879f8c88a7aa4214e1", "cve": "CVE-2021-23348", "cwe": "CWE-78", "file": "portprocesses-1-0-4/fixed.js", "function": "listProcessesOnPort", "id": "portprocesses-1-0-4:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "portprocesses-1-0-4", "project": "portprocesses", "provenance": "human", "relpath": "index.js", "repo": "rrainn/PortProcesses", "split": "train", "year": 2021} +{"class": "path traversal", "commit": "ad077637bf7a9fe0dee3262f011aed19a45ce92c", "commit_url": "https://github.com/ChristoPy/serve-here.js/commit/cefb51d03290b6a88dd13143ab2de31b8cf57c39", "cve": "CVE-2019-5444", "cwe": "CWE-22", "file": "serve-here-js-1-1-0/vuln.js", "function": "ConfigureFilePath", "id": "serve-here-js-1-1-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "path_join_user_input", "pair": "serve-here-js-1-1-0", "project": "serve-here.js", "provenance": "human", "relpath": "source/response.js", "repo": "ChristoPy/serve-here.js", "split": "holdout", "year": 2019} +{"class": "path traversal", "commit": "cefb51d03290b6a88dd13143ab2de31b8cf57c39", "commit_url": "https://github.com/ChristoPy/serve-here.js/commit/cefb51d03290b6a88dd13143ab2de31b8cf57c39", "cve": "CVE-2019-5444", "cwe": "CWE-22", "file": "serve-here-js-1-1-0/fixed.js", "function": "ConfigureFilePath", "id": "serve-here-js-1-1-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "path_join_user_input", "pair": "serve-here-js-1-1-0", "project": "serve-here.js", "provenance": "human", "relpath": "source/response.js", "repo": "ChristoPy/serve-here.js", "split": "holdout", "year": 2019} +{"class": "code injection", "commit": "5bba7905b0e797460b8f6c2e38ff6869146faab3", "commit_url": "https://github.com/browserify/static-eval/commit/32f8efd072625e267aa7237d2daff35738c50d2a", "cve": "", "cwe": "CWE-94", "file": "static-eval-1-1-1/vuln.js", "function": "walk", "id": "static-eval-1-1-1:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "static-eval-1-1-1", "project": "static-eval", "provenance": "human", "relpath": "index.js", "repo": "browserify/static-eval", "split": "train", "year": 2019} +{"class": "code injection", "commit": "32f8efd072625e267aa7237d2daff35738c50d2a", "commit_url": "https://github.com/browserify/static-eval/commit/32f8efd072625e267aa7237d2daff35738c50d2a", "cve": "", "cwe": "CWE-94", "file": "static-eval-1-1-1/fixed.js", "function": "walk", "id": "static-eval-1-1-1:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "static-eval-1-1-1", "project": "static-eval", "provenance": "human", "relpath": "index.js", "repo": "browserify/static-eval", "split": "train", "year": 2019} +{"class": "code injection", "commit": "aa21cfb64f47238c764d258d0faf7366a8487e36", "commit_url": "https://github.com/thenables/thenify/commit/0d94a24eb933bc835d568f3009f4d269c4c4c17a", "cve": "CVE-2020-7677", "cwe": "CWE-94", "file": "thenify-3-3-0/vuln.js", "function": "thenify", "id": "thenify-3-3-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "thenify-3-3-0", "project": "thenify", "provenance": "human", "relpath": "index.js", "repo": "thenables/thenify", "split": "holdout", "year": 2020} +{"class": "code injection", "commit": "0d94a24eb933bc835d568f3009f4d269c4c4c17a", "commit_url": "https://github.com/thenables/thenify/commit/0d94a24eb933bc835d568f3009f4d269c4c4c17a", "cve": "CVE-2020-7677", "cwe": "CWE-94", "file": "thenify-3-3-0/fixed.js", "function": "thenify", "id": "thenify-3-3-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "thenify-3-3-0", "project": "thenify", "provenance": "human", "relpath": "index.js", "repo": "thenables/thenify", "split": "holdout", "year": 2020} +{"class": "code injection", "commit": "ef646ccbbbd971e82719c83fc69f56a5585b3a01", "commit_url": "https://github.com/jashkenas/underscore/commit/4c73526d43838ad6ab43a6134728776632adeb66", "cve": "CVE-2021-23358", "cwe": "CWE-94", "file": "underscore-1-13-0-0/vuln.js", "function": "template", "id": "underscore-1-13-0-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "underscore-1-13-0-0", "project": "underscore", "provenance": "human", "relpath": "modules/template.js", "repo": "jashkenas/underscore", "split": "train", "year": 2021} +{"class": "code injection", "commit": "4c73526d43838ad6ab43a6134728776632adeb66", "commit_url": "https://github.com/jashkenas/underscore/commit/4c73526d43838ad6ab43a6134728776632adeb66", "cve": "CVE-2021-23358", "cwe": "CWE-94", "file": "underscore-1-13-0-0/fixed.js", "function": "template", "id": "underscore-1-13-0-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "underscore-1-13-0-0", "project": "underscore", "provenance": "human", "relpath": "modules/template.js", "repo": "jashkenas/underscore", "split": "train", "year": 2021} +{"class": "command injection", "commit": "b8b642b1163baa37f15ed49b4064dcbdb6e1876c", "commit_url": "https://github.com/vvo/node-whereis/commit/0f64e3780235004fb6e43bfd153ea3e0e210ee2b", "cve": "CVE-2018-3772", "cwe": "CWE-78", "file": "whereis-0-4-0/vuln.js", "function": "whereis", "id": "whereis-0-4-0:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "whereis-0-4-0", "project": "whereis", "provenance": "human", "relpath": "index.js", "repo": "vvo/node-whereis", "split": "holdout", "year": 2018} +{"class": "command injection", "commit": "0f64e3780235004fb6e43bfd153ea3e0e210ee2b", "commit_url": "https://github.com/vvo/node-whereis/commit/0f64e3780235004fb6e43bfd153ea3e0e210ee2b", "cve": "CVE-2018-3772", "cwe": "CWE-78", "file": "whereis-0-4-0/fixed.js", "function": "whereis", "id": "whereis-0-4-0:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "whereis-0-4-0", "project": "whereis", "provenance": "human", "relpath": "index.js", "repo": "vvo/node-whereis", "split": "holdout", "year": 2018} diff --git a/benchmarks/pairs/vfc-js/underscore-1-13-0-0/fixed.js b/benchmarks/pairs/vfc-js/underscore-1-13-0-0/fixed.js new file mode 100644 index 0000000..06f5e7a --- /dev/null +++ b/benchmarks/pairs/vfc-js/underscore-1-13-0-0/fixed.js @@ -0,0 +1,77 @@ +// Provenance: jashkenas/underscore (fixed). +// repo: jashkenas/underscore +// commit: 4c73526d43838ad6ab43a6134728776632adeb66 +// parent: ef646ccbbbd971e82719c83fc69f56a5585b3a01 +// commit_url: https://github.com/jashkenas/underscore/commit/4c73526d43838ad6ab43a6134728776632adeb66 +// cve: CVE-2021-23358 +// license: MIT +// function: template +// relpath: modules/template.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 38 + +export default function template(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offset. + return match; + }); + source += "';\n"; + + var argument = settings.variable; + if (argument) { + // Insure against third-party code injection. + if (!bareIdentifier.test(argument)) throw new Error( + 'variable is not a bare identifier: ' + argument + ); + } else { + // If a variable is not specified, place data values in local scope. + source = 'with(obj||{}){\n' + source + '}\n'; + argument = 'obj'; + } + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(argument, '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; +} diff --git a/benchmarks/pairs/vfc-js/underscore-1-13-0-0/vuln.js b/benchmarks/pairs/vfc-js/underscore-1-13-0-0/vuln.js new file mode 100644 index 0000000..0ea4df0 --- /dev/null +++ b/benchmarks/pairs/vfc-js/underscore-1-13-0-0/vuln.js @@ -0,0 +1,69 @@ +// Provenance: jashkenas/underscore (vuln). +// repo: jashkenas/underscore +// commit: ef646ccbbbd971e82719c83fc69f56a5585b3a01 +// parent: ef646ccbbbd971e82719c83fc69f56a5585b3a01 +// commit_url: https://github.com/jashkenas/underscore/commit/4c73526d43838ad6ab43a6134728776632adeb66 +// cve: CVE-2021-23358 +// license: MIT +// function: template +// relpath: modules/template.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 31 + +export default function template(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offset. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; +} diff --git a/benchmarks/pairs/vfc-js/whereis-0-4-0/fixed.js b/benchmarks/pairs/vfc-js/whereis-0-4-0/fixed.js new file mode 100644 index 0000000..0a1da86 --- /dev/null +++ b/benchmarks/pairs/vfc-js/whereis-0-4-0/fixed.js @@ -0,0 +1,42 @@ +// Provenance: vvo/node-whereis (fixed). +// repo: vvo/node-whereis +// commit: 0f64e3780235004fb6e43bfd153ea3e0e210ee2b +// parent: b8b642b1163baa37f15ed49b4064dcbdb6e1876c +// commit_url: https://github.com/vvo/node-whereis/commit/0f64e3780235004fb6e43bfd153ea3e0e210ee2b +// cve: CVE-2018-3772 +// license: MIT +// function: whereis +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 3 + +module.exports = function whereis(name, cb) { + cp.execFile('which', [name], function(error, stdout, stderr) { + stdout = stdout.split('\n')[0]; + if (error || stderr || stdout === '' || stdout.charAt(0) !== '/') { + stdout = stdout.split('\n')[0]; + cp.execFile('whereis', [name], function(error, stdout, stderr) { + if (error || stderr || stdout === '' || stdout.indexOf( '/' ) === -1) { + cp.execFile('where', [name], function (error, stdout, stderr) { //windows + if (error || stderr || stdout === '' || stdout.indexOf('\\') === -1) { + cp.exec('for %i in (' + name + '.exe) do @echo. %~$PATH:i', function (error, stdout, stderr) { //windows xp + if (error || stderr || stdout === '' || stdout.indexOf('\\') === -1) { + return cb(new Error('Could not find ' + name + ' on your system')); + } + return cb(null, stdout); + }); + } else { + return cb(null, stdout); + } + }); + } + else { + return cb(null, stdout.split(' ')[1]); + } + }); + } else { + return cb(null, stdout); + } + }); +}; diff --git a/benchmarks/pairs/vfc-js/whereis-0-4-0/vuln.js b/benchmarks/pairs/vfc-js/whereis-0-4-0/vuln.js new file mode 100644 index 0000000..e07f171 --- /dev/null +++ b/benchmarks/pairs/vfc-js/whereis-0-4-0/vuln.js @@ -0,0 +1,42 @@ +// Provenance: vvo/node-whereis (vuln). +// repo: vvo/node-whereis +// commit: b8b642b1163baa37f15ed49b4064dcbdb6e1876c +// parent: b8b642b1163baa37f15ed49b4064dcbdb6e1876c +// commit_url: https://github.com/vvo/node-whereis/commit/0f64e3780235004fb6e43bfd153ea3e0e210ee2b +// cve: CVE-2018-3772 +// license: MIT +// function: whereis +// relpath: index.js +// provenance: human +// mechanism: source_reaches_sink +// upstream_start: 3 + +module.exports = function whereis(name, cb) { + cp.exec('which ' + name, function(error, stdout, stderr) { + stdout = stdout.split('\n')[0]; + if (error || stderr || stdout === '' || stdout.charAt(0) !== '/') { + stdout = stdout.split('\n')[0]; + cp.exec('whereis ' + name, function(error, stdout, stderr) { + if (error || stderr || stdout === '' || stdout.indexOf( '/' ) === -1) { + cp.exec('where ' + name, function (error, stdout, stderr) { //windows + if (error || stderr || stdout === '' || stdout.indexOf('\\') === -1) { + cp.exec('for %i in (' + name + '.exe) do @echo. %~$PATH:i', function (error, stdout, stderr) { //windows xp + if (error || stderr || stdout === '' || stdout.indexOf('\\') === -1) { + return cb(new Error('Could not find ' + name + ' on your system')); + } + return cb(null, stdout); + }); + } else { + return cb(null, stdout); + } + }); + } + else { + return cb(null, stdout.split(' ')[1]); + } + }); + } else { + return cb(null, stdout); + } + }); +}; diff --git a/benchmarks/pairs/vibe-py/README.md b/benchmarks/pairs/vibe-py/README.md new file mode 100644 index 0000000..6ccae58 --- /dev/null +++ b/benchmarks/pairs/vibe-py/README.md @@ -0,0 +1,23 @@ +# vibe-py slice + +Isolated Python web functions from [Real-Vuln-Benchmark](https://github.com/kolega-ai/Real-Vuln-Benchmark) +(Apache-2.0 benchmark; each target repository keeps its own license). + +- **Vuln side**: the function enclosing a labeled vulnerable finding (`is_vulnerable = true`). +- **Fixed twin**: a false-positive trap function (`is_vulnerable = false`) from the same repository. + The scanner must stay silent there, so silent-on-fix here measures precision on traps. +- **Provenance**: `human` for the 26 human-authored apps, `agent` for the 40 LLM-generated apps. + The LLM-generated repositories (`kolega-ai-dev/vc-*`) currently carry **no license file**, + so they are skipped by `build_recipes.py` and not vendored (Req 5.4). Re-run with + `--allow-unlicensed` only to inspect them locally; do not commit those excerpts. +- **Caveat**: the human corpus is educational and CTF style (DVPWA, VAmPI, vulpy, ...), not + production code. Say so when reading numbers. +- **Matching**: every row names the function; the upstream scorer tolerates ±10 lines, ours + requires the detection inside the function. + +- **Excerpt shape**: line-range excerpts of methods keep their class indentation, so 27 of them + fail stdlib `ast.parse` ("unexpected indent") while tree-sitter parses them with correct + function ranges. `parse_failed 0` rests on tree-sitter; stdlib-ast parity would need dedenting. + +Rebuild: see the docstring of `build_recipes.py`. Regenerate the catalog with +`python benchmarks/pairs/catalog_gen.py --slice vibe-py`. Maintainer only; CI never fetches. diff --git a/benchmarks/pairs/vibe-py/build_recipes.py b/benchmarks/pairs/vibe-py/build_recipes.py new file mode 100644 index 0000000..26d824e --- /dev/null +++ b/benchmarks/pairs/vibe-py/build_recipes.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Build vibe-py recipes from Real-Vuln-Benchmark ground truth (maintainer, offline once downloaded). + +Download first (network, maintainer only): + + mkdir -p /tmp/realvuln + gh api repos/kolega-ai/Real-Vuln-Benchmark/contents/benchmark-manifest.json --jq .content | base64 -d > /tmp/realvuln/manifest.json + for r in $(gh api repos/kolega-ai/Real-Vuln-Benchmark/contents/ground-truth --jq '.[] | select(.type=="dir") | .name'); do + gh api "repos/kolega-ai/Real-Vuln-Benchmark/contents/ground-truth/$r/ground-truth.json" --jq .content | base64 -d > "/tmp/realvuln/$r.json" + done + +Then: + + python benchmarks/pairs/vibe-py/build_recipes.py --ground-truth /tmp/realvuln --licenses /tmp/realvuln/licenses.json + python benchmarks/pairs/harvest.py --slice vibe-py --all --fetch + python benchmarks/pairs/catalog_gen.py --slice vibe-py + +Pair shape: the vulnerable function (enclosing the labeled sink line) is the vuln side; a +false-positive trap function from the same repository is the fixed twin. Repositories +without a stated license are skipped (Req 5.4). Every recipe is a candidate until the +catalog row is reviewed. +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +MECHANISM = { + "idor": "identity_from_request_body", + "broken_access_control": "missing_auth_guard", + "mass_assignment": "identity_from_request_body", + "path_traversal": "path_join_user_input", + "unrestricted_file_upload": "path_join_user_input", + "hardcoded_credentials": "secret_in_client", + "sensitive_data_exposure": "secret_in_client", + "security_misconfiguration": "permissive_default", + "insecure_cookie_flags": "permissive_default", + "csrf": "permissive_default", + "weak_prng": "config_sink_weak_literal", + "weak_crypto": "config_sink_weak_literal", + "weak_hashing": "config_sink_weak_literal", + "sql_injection": "source_reaches_sink", + "nosql_injection": "source_reaches_sink", + "command_injection": "source_reaches_sink", + "code_injection": "source_reaches_sink", + "ssti": "source_reaches_sink", + "ssrf": "source_reaches_sink", + "open_redirect": "source_reaches_sink", + "reflected_xss": "source_reaches_sink", + "stored_xss": "source_reaches_sink", + "xxe": "source_reaches_sink", + "insecure_deserialization": "source_reaches_sink", + "sensitive_data_in_logs": "secret_in_client", + "business_logic_validation_gap": "validation_strength", + "user_enumeration": "validation_strength", + "missing_rate_limiting": "permissive_default", + "missing_rate_limiting_on_auth": "permissive_default", + "denial_of_service": "validation_strength", +} +# Classes an intra-file engine can talk about; the rest are recorded as candidates but not selected by default. +DEFAULT_CLASSES = ( + "sql_injection", + "command_injection", + "code_injection", + "ssti", + "ssrf", + "path_traversal", + "open_redirect", + "insecure_deserialization", + "idor", + "broken_access_control", + "hardcoded_credentials", + "security_misconfiguration", + "reflected_xss", + "xxe", + "nosql_injection", +) +ACCEPTED_LICENSES = {"MIT", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-3.0", "GPL-2.0", "MPL-2.0", "Unlicense", "ISC"} + + +def _slug(text: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") + + +def build( + ground_truth: Path, licenses: Path, *, per_repo: int, classes: set[str], allow_unlicensed: bool +) -> tuple[list[dict[str, object]], dict[str, int]]: + manifest = json.loads((ground_truth / "manifest.json").read_text()) + license_by_repo = json.loads(licenses.read_text()) if licenses.is_file() else {} + recipes: list[dict[str, object]] = [] + skipped = {"unlicensed": 0, "no_trap": 0, "no_function": 0, "class": 0} + for repo_id, meta in sorted(manifest["repos"].items()): + gt_path = ground_truth / f"{repo_id}.json" + if not gt_path.is_file(): + continue + gt = json.loads(gt_path.read_text()) + info = license_by_repo.get(repo_id, {}) + license_id = info.get("license") or "" + if license_id not in ACCEPTED_LICENSES and not allow_unlicensed: + skipped["unlicensed"] += 1 + continue + full = info.get("full") or "/".join(meta["repo_url"].rstrip("/").split("/")[-2:]) + commit = meta["commit_sha"] + provenance = "agent" if meta["authorship"] == "llm_generated" else "human" + findings = gt.get("findings", []) + traps = [ + f + for f in findings + if not f.get("is_vulnerable") and f.get("location", {}).get("function") and f.get("file", "").endswith(".py") + ] + if not traps: + skipped["no_trap"] += 1 + continue + taken = 0 + seen_functions: set[tuple[str, str]] = set() + for finding in findings: + if taken >= per_repo: + break + if not finding.get("is_vulnerable"): + continue + location = finding.get("location", {}) + function = location.get("function") + file = finding.get("file", "") + if not function or not file.endswith(".py"): + skipped["no_function"] += 1 + continue + vclass = str(finding.get("vulnerability_class", "")) + if vclass not in classes: + skipped["class"] += 1 + continue + if (file, function) in seen_functions: + continue + seen_functions.add((file, function)) + trap = next((t for t in traps if t["file"] == file), traps[0]) + name = f"{_slug(repo_id.replace('realvuln-', '').replace('vc-', ''))}-{_slug(Path(file).stem)}-{_slug(function)}"[:60] + while any(r["name"] == name for r in recipes): + name = name[:57] + "-" + str(sum(1 for r in recipes if str(r["name"]).startswith(name[:57])) + 1) + recipes.append( + { + "name": name, + "host": "github", + "repo": full, + "parent": commit, + "commit": commit, + "path": file, + "fix_path": trap["file"], + "mode": "enclosing", + "line": int(location["start_line"]), + "fix_line": int(trap["location"]["start_line"]), + "function": function, + "language": "python", + "relpath": file, + "license": license_id or "unlicensed", + "cve": str(finding.get("evidence", {}).get("cve_id") or ""), + "year": 2026, + "cwe": str(finding.get("primary_cwe", "")), + "class": vclass, + "mechanism": MECHANISM.get(vclass, "other"), + "provenance": provenance, + "origin": "real-vuln-benchmark", + "project": repo_id, + "framework": str(meta.get("framework", "")), + "gt_id": str(finding.get("id", "")), + "trap_id": str(trap.get("id", "")), + "acceptable_cwes": list(finding.get("acceptable_cwes", [])), + "evidence": str(finding.get("evidence", {}).get("description", ""))[:240].replace("\n", " "), + "commit_url": f"https://github.com/{full}/blob/{commit}/{file}#L{int(location['start_line'])}", + } + ) + taken += 1 + # deterministic holdout: every other recipe by sorted name + for index, recipe in enumerate(sorted(recipes, key=lambda r: str(r["name"]))): + recipe["split"] = "holdout" if index % 2 else "train" + return recipes, skipped + + +def to_toml(recipes: list[dict[str, object]]) -> str: + lines = [ + "# vibe-py harvest recipes generated from Real-Vuln-Benchmark ground truth by build_recipes.py.", + "# Vulnerable function = vuln side; a false-positive trap from the same repo = fixed twin.", + "# Review each row before trusting it as a label; regenerate with build_recipes.py.", + "", + ] + for recipe in sorted(recipes, key=lambda r: str(r["name"])): + lines.append("[[recipe]]") + for key, value in recipe.items(): + if isinstance(value, bool): + lines.append(f"{key} = {'true' if value else 'false'}") + elif isinstance(value, int): + lines.append(f"{key} = {value}") + elif isinstance(value, list): + inner = ", ".join('"' + str(item).replace('"', '\\"') + '"' for item in value) + lines.append(f"{key} = [{inner}]") + else: + lines.append(f'{key} = "{str(value).replace(chr(92), chr(92) * 2).replace(chr(34), chr(92) + chr(34))}"') + lines.append("") + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--ground-truth", type=Path, required=True, help="directory with manifest.json and .json ground truth") + parser.add_argument("--licenses", type=Path, default=None, help="json map repo_id -> {full, license}") + parser.add_argument("--per-repo", type=int, default=2) + parser.add_argument("--classes", default=",".join(DEFAULT_CLASSES)) + parser.add_argument("--allow-unlicensed", action="store_true", help="include repositories without a stated license (NOT for vendoring)") + parser.add_argument("--out", type=Path, default=ROOT / "recipes.toml") + args = parser.parse_args(argv) + recipes, skipped = build( + args.ground_truth, + args.licenses or (args.ground_truth / "licenses.json"), + per_repo=args.per_repo, + classes=set(args.classes.split(",")), + allow_unlicensed=args.allow_unlicensed, + ) + args.out.write_text(to_toml(recipes), encoding="utf-8") + print(f"wrote {args.out} ({len(recipes)} recipes); skipped {skipped}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/pairs/vibe-py/catalog.toml b/benchmarks/pairs/vibe-py/catalog.toml new file mode 100644 index 0000000..48fb39e --- /dev/null +++ b/benchmarks/pairs/vibe-py/catalog.toml @@ -0,0 +1,984 @@ +# vibe-py pair slice. Reviewed isolated functions with provenance. +# Honesty dashboard — not the local 100% CI gate. +# Do not add these names to LANGUAGE_MANIFESTS. +# Regenerated by benchmarks/pairs/catalog_gen.py from recipes.toml. + +[[pair]] +name = "damn-vulnerable-graphql-application-views-mutate" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "dolevf/Damn-Vulnerable-GraphQL-Application" +cve = "" +cwe = "CWE-78" +commit = "a961308c02d1fb462b192681c336b0739e432da7" +parent = "a961308c02d1fb462b192681c336b0739e432da7" +commit_url = "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L211" +vuln = "damn-vulnerable-graphql-application-views-mutate/vuln.py" +fixed = "damn-vulnerable-graphql-application-views-mutate/fixed.py" +relpath = "core/views.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command_injection" +path = "core/views.py" +function = "mutate" +mechanism = "source_reaches_sink" +evidence = "OS command injection in ImportPaste mutation. User-controlled host, port, path, and scheme parameters are used to construct a URL that is passed to os.popen via helpers.run_cmd(f'curl --insecure {url}'). In easy mode, strip_dangerous_charac" + +[[pair]] +name = "damn-vulnerable-graphql-application-views-resolve-pastes" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "dolevf/Damn-Vulnerable-GraphQL-Application" +cve = "" +cwe = "CWE-89" +commit = "a961308c02d1fb462b192681c336b0739e432da7" +parent = "a961308c02d1fb462b192681c336b0739e432da7" +commit_url = "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L320" +vuln = "damn-vulnerable-graphql-application-views-resolve-pastes/vuln.py" +fixed = "damn-vulnerable-graphql-application-views-resolve-pastes/fixed.py" +relpath = "core/views.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "core/views.py" +function = "resolve_pastes" +mechanism = "source_reaches_sink" +evidence = "SQL injection via unsanitized 'filter' GraphQL argument directly interpolated into raw SQL using Python string formatting: result.filter(text(\"title = '%s' or content = '%s'\" % (filter, filter))). An attacker can inject arbitrary SQL via th" + +[[pair]] +name = "damn-vulnerable-graphql-application-views-resolve-system-dia" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "dolevf/Damn-Vulnerable-GraphQL-Application" +cve = "" +cwe = "CWE-78" +commit = "a961308c02d1fb462b192681c336b0739e432da7" +parent = "a961308c02d1fb462b192681c336b0739e432da7" +commit_url = "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L345" +vuln = "damn-vulnerable-graphql-application-views-resolve-system-dia/vuln.py" +fixed = "damn-vulnerable-graphql-application-views-resolve-system-dia/fixed.py" +relpath = "core/views.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command_injection" +path = "core/views.py" +function = "resolve_system_diagnostics" +mechanism = "source_reaches_sink" +evidence = "OS command injection via the 'cmd' argument in the systemDiagnostics GraphQL query. In easy mode, security.allowed_cmds() always returns True, so user-supplied cmd is passed directly to os.popen via helpers.run_cmd(cmd). Requires valid admi" + +[[pair]] +name = "djangoat-analytics-hits-by-ip" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "Contrast-Security-OSS/DjanGoat" +cve = "" +cwe = "CWE-89" +commit = "082c88bfefced508ae083b92b459c808f95e905e" +parent = "082c88bfefced508ae083b92b459c808f95e905e" +commit_url = "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/models/Analytics/analytics.py#L57" +vuln = "djangoat-analytics-hits-by-ip/vuln.py" +fixed = "djangoat-analytics-hits-by-ip/fixed.py" +relpath = "app/models/Analytics/analytics.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "app/models/Analytics/analytics.py" +function = "hits_by_ip" +mechanism = "source_reaches_sink" +evidence = "SQL injection via string formatting in raw query. Both ip and col parameters are interpolated directly into SQL: 'SELECT %s FROM %s WHERE ip_address=\\'%s\\' ORDER BY id DESC' % (col, table_name, ip). The ip parameter comes from request.GET['" + +[[pair]] +name = "djangoat-views-reset-password" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "Contrast-Security-OSS/DjanGoat" +cve = "" +cwe = "CWE-502" +commit = "082c88bfefced508ae083b92b459c808f95e905e" +parent = "082c88bfefced508ae083b92b459c808f95e905e" +commit_url = "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/password_resets/views.py#L84" +vuln = "djangoat-views-reset-password/vuln.py" +fixed = "djangoat-views-reset-password/fixed.py" +relpath = "app/views/password_resets/views.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-502" +class = "insecure_deserialization" +path = "app/views/password_resets/views.py" +function = "reset_password" +mechanism = "source_reaches_sink" +evidence = "Remote code execution via insecure deserialization. User-controlled POST parameter 'user' is base64-decoded and passed to pickle.loads(): user = pickle.loads(base64.b64decode(encoded_user)). An attacker can craft a malicious pickle payload " + +[[pair]] +name = "djangoat-views-user-view" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "Contrast-Security-OSS/DjanGoat" +cve = "" +cwe = "CWE-89" +commit = "082c88bfefced508ae083b92b459c808f95e905e" +parent = "082c88bfefced508ae083b92b459c808f95e905e" +commit_url = "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/users/views.py#L74" +vuln = "djangoat-views-user-view/vuln.py" +fixed = "djangoat-views-user-view/fixed.py" +relpath = "app/views/users/views.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "app/views/users/views.py" +function = "user_view" +mechanism = "source_reaches_sink" +evidence = "SQL injection via string formatting in raw query. user_id_form from POST data is interpolated directly into SQL: User.objects.raw('SELECT * FROM %s WHERE user_id=\\'%s\\' ORDER BY is_admin=\\'0\\'' % (table_name, user_id_form)). An attacker can" + +[[pair]] +name = "dsvpwa-attacks-run" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "sgabe/DSVPWA" +cve = "" +cwe = "CWE-89" +commit = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +parent = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +commit_url = "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/attacks.py#L39" +vuln = "dsvpwa-attacks-run/vuln.py" +fixed = "dsvpwa-attacks-run/fixed.py" +relpath = "dsvpwa/attacks.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "dsvpwa/attacks.py" +function = "run" +mechanism = "source_reaches_sink" +evidence = "SQL injection via string concatenation in SQLinjection.run(). The 'id' parameter from request is concatenated directly into SQL: cursor.execute(\"SELECT ... WHERE id=\" + id). No parameterization or input validation." + +[[pair]] +name = "dsvpwa-dsvpwa-main" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "sgabe/DSVPWA" +cve = "" +cwe = "CWE-326" +commit = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +parent = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +commit_url = "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa.py#L37" +vuln = "dsvpwa-dsvpwa-main/vuln.py" +fixed = "dsvpwa-dsvpwa-main/fixed.py" +relpath = "dsvpwa.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-326" +class = "security_misconfiguration" +path = "dsvpwa.py" +function = "main" +mechanism = "permissive_default" +evidence = "Insecure SSL/TLS configuration in main(). Three dangerous SSL options are explicitly enabled: (1) SSLv3 re-enabled via ctx.options &= ~ssl.OP_NO_SSLv3 (vulnerable to POODLE), (2) TLS compression re-enabled via &= ~ssl.OP_NO_COMPRESSION (vul" + +[[pair]] +name = "dsvpwa-handlers-do-bdr" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "sgabe/DSVPWA" +cve = "" +cwe = "CWE-78" +commit = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +parent = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +commit_url = "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/handlers.py#L183" +vuln = "dsvpwa-handlers-do-bdr/vuln.py" +fixed = "dsvpwa-handlers-do-bdr/fixed.py" +relpath = "dsvpwa/handlers.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command_injection" +path = "dsvpwa/handlers.py" +function = "do_BDR" +mechanism = "source_reaches_sink" +evidence = "Critical command injection via custom HTTP method BDR in do_BDR(). The URL path (self.path[1:]) is passed directly to subprocess.check_output() with shell=True: subprocess.check_output(self.path[1:], shell=True, ...). Any command can be exe" + +[[pair]] +name = "dsvw-dsvw-do-get" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "Unlicense" +repo = "stamparm/DSVW" +cve = "" +cwe = "CWE-89" +commit = "7d40f4b7939c901610ed9b85724552d60e7d63fa" +parent = "7d40f4b7939c901610ed9b85724552d60e7d63fa" +commit_url = "https://github.com/stamparm/DSVW/blob/7d40f4b7939c901610ed9b85724552d60e7d63fa/dsvw.py#L30" +vuln = "dsvw-dsvw-do-get/vuln.py" +fixed = "dsvw-dsvw-do-get/fixed.py" +relpath = "dsvw.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "dsvw.py" +function = "do_GET" +mechanism = "source_reaches_sink" +evidence = "SQL injection via id parameter directly concatenated into SELECT query without sanitization" + +[[pair]] +name = "dvpwa-app-init" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "anxolerd/dvpwa" +cve = "" +cwe = "CWE-16" +commit = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +parent = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +commit_url = "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/app.py#L35" +vuln = "dvpwa-app-init/vuln.py" +fixed = "dvpwa-app-init/fixed.py" +relpath = "sqli/app.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-16" +class = "security_misconfiguration" +path = "sqli/app.py" +function = "init" +mechanism = "permissive_default" +evidence = "Jinja2 template engine configured with autoescape=False. This disables automatic HTML escaping for all template variables, making every unescaped template expression a potential XSS sink. This is the root cause of all stored XSS vulnerabili" + +[[pair]] +name = "dvpwa-middlewares-session-middleware" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "anxolerd/dvpwa" +cve = "" +cwe = "CWE-1004" +commit = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +parent = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +commit_url = "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/middlewares.py#L20" +vuln = "dvpwa-middlewares-session-middleware/vuln.py" +fixed = "dvpwa-middlewares-session-middleware/fixed.py" +relpath = "sqli/middlewares.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-1004" +class = "security_misconfiguration" +path = "sqli/middlewares.py" +function = "session_middleware" +mechanism = "permissive_default" +evidence = "Session cookie created with httponly=False. The RedisStorage is configured with httponly=False, meaning the AIOHTTP_SESSION cookie is accessible to JavaScript via document.cookie. Combined with the stored XSS vulnerabilities, this enables s" + +[[pair]] +name = "dvpwa-student-create" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "anxolerd/dvpwa" +cve = "" +cwe = "CWE-89" +commit = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +parent = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +commit_url = "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/dao/student.py#L42" +vuln = "dvpwa-student-create/vuln.py" +fixed = "dvpwa-student-create/fixed.py" +relpath = "sqli/dao/student.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "sqli/dao/student.py" +function = "create" +mechanism = "source_reaches_sink" +evidence = "SQL injection via Python string formatting. The 'name' parameter from user input is interpolated directly into the SQL query using Python's % operator: \"VALUES ('%(name)s')\" % {'name': name}. The resulting string is then passed to cur.execu" + +[[pair]] +name = "extremely-vulnerable-flask-app-account-before-request" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "GPL-3.0" +repo = "manuelz120/extremely-vulnerable-flask-app" +cve = "" +cwe = "CWE-502" +commit = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +parent = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +commit_url = "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L118" +vuln = "extremely-vulnerable-flask-app-account-before-request/vuln.py" +fixed = "extremely-vulnerable-flask-app-account-before-request/fixed.py" +relpath = "routes/account.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-502" +class = "insecure_deserialization" +path = "routes/account.py" +function = "before_request" +mechanism = "source_reaches_sink" +evidence = "Remote code execution via insecure deserialization. The 'preferences' cookie is base64-decoded and passed directly to pickle.loads() (imported as 'loads' from pickle). An attacker can craft a malicious pickle payload, base64-encode it, and " + +[[pair]] +name = "extremely-vulnerable-flask-app-account-search" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "GPL-3.0" +repo = "manuelz120/extremely-vulnerable-flask-app" +cve = "" +cwe = "CWE-89" +commit = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +parent = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +commit_url = "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L33" +vuln = "extremely-vulnerable-flask-app-account-search/vuln.py" +fixed = "extremely-vulnerable-flask-app-account-search/fixed.py" +relpath = "routes/account.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "routes/account.py" +function = "search" +mechanism = "source_reaches_sink" +evidence = "SQL injection via f-string interpolation of user-supplied search parameter into a raw SQL text() fragment: text(f\"text like '%{search_param}%'\"). The search_param comes directly from request.args.get('search') with no sanitization." + +[[pair]] +name = "extremely-vulnerable-flask-app-signup-validate-token" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "GPL-3.0" +repo = "manuelz120/extremely-vulnerable-flask-app" +cve = "" +cwe = "CWE-89" +commit = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +parent = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +commit_url = "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/signup.py#L15" +vuln = "extremely-vulnerable-flask-app-signup-validate-token/vuln.py" +fixed = "extremely-vulnerable-flask-app-signup-validate-token/fixed.py" +relpath = "routes/signup.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "routes/signup.py" +function = "validate_token" +mechanism = "source_reaches_sink" +evidence = "SQL injection via f-string interpolation of user-supplied registration code directly into a raw SQL query: text(f\"SELECT id, code FROM ... WHERE code = '{code}'\"). An attacker can inject arbitrary SQL through the registration_code form fiel" + +[[pair]] +name = "python-insecure-app-main-try-hack-me" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "GPL-3.0" +repo = "kolega-ai/realvuln-python-insecure-app" +cve = "" +cwe = "CWE-1336" +commit = "80b894a2f754590747a31c92b2088be086421953" +parent = "80b894a2f754590747a31c92b2088be086421953" +commit_url = "https://github.com/kolega-ai/realvuln-python-insecure-app/blob/80b894a2f754590747a31c92b2088be086421953/app/main.py#L41" +vuln = "python-insecure-app-main-try-hack-me/vuln.py" +fixed = "python-insecure-app-main-try-hack-me/fixed.py" +relpath = "app/main.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-1336" +class = "ssti" +path = "app/main.py" +function = "try_hack_me" +mechanism = "source_reaches_sink" +evidence = "Server-Side Template Injection via Jinja2. User-controlled 'name' query parameter is interpolated into an f-string on line 38 to build 'content', which is then passed to Template(content).render() on line 41. An attacker can inject Jinja2 t" + +[[pair]] +name = "pythonssti-main-read-root" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "TheWation/PythonSSTI" +cve = "" +cwe = "CWE-79" +commit = "76339c27d5a870c0a81bad6aee5f9bd516592145" +parent = "76339c27d5a870c0a81bad6aee5f9bd516592145" +commit_url = "https://github.com/TheWation/PythonSSTI/blob/76339c27d5a870c0a81bad6aee5f9bd516592145/main.py#L14" +vuln = "pythonssti-main-read-root/vuln.py" +fixed = "pythonssti-main-read-root/fixed.py" +relpath = "main.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-79" +class = "reflected_xss" +path = "main.py" +function = "read_root" +mechanism = "source_reaches_sink" +evidence = "The Jinja2 Environment is created with default settings (autoescape=False). User input from the 'username' query parameter is concatenated into the template string and the rendered output is returned as HTMLResponse. An attacker can inject " + +[[pair]] +name = "threatbyte-api-v1-delete" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "anotherik/ThreatByte" +cve = "" +cwe = "CWE-862" +commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L223" +vuln = "threatbyte-api-v1-delete/vuln.py" +fixed = "threatbyte-api-v1-delete/fixed.py" +relpath = "server/api/v1/api_v1.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-862" +class = "broken_access_control" +path = "server/api/v1/api_v1.py" +function = "delete" +mechanism = "missing_auth_guard" +evidence = "BFLA (Broken Function Level Authorization) on the delete-user API endpoint. The @token_required decorator is commented out (lines 221-222), so DELETE /api/v1/delete-user/ can be called by anyone without authentication to delete any" + +[[pair]] +name = "threatbyte-api-v1-get" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "anotherik/ThreatByte" +cve = "" +cwe = "CWE-639" +commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L154" +vuln = "threatbyte-api-v1-get/vuln.py" +fixed = "threatbyte-api-v1-get/fixed.py" +relpath = "server/api/v1/api_v1.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-639" +class = "broken_access_control" +path = "server/api/v1/api_v1.py" +function = "get" +mechanism = "missing_auth_guard" +evidence = "BOLA (Broken Object Level Authorization) on the API profile endpoint. The @token_required decorator is commented out (line 152-153), so /api/v1/profile?user_id=X returns any user's profile (username, email, country, role, permissions, team)" + +[[pair]] +name = "threatbyte-routes-dashboard" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "anotherik/ThreatByte" +cve = "" +cwe = "CWE-89" +commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/routes.py#L137" +vuln = "threatbyte-routes-dashboard/vuln.py" +fixed = "threatbyte-routes-dashboard/fixed.py" +relpath = "server/routes.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "server/routes.py" +function = "dashboard" +mechanism = "source_reaches_sink" +evidence = "SQL injection via f-string interpolation of user-controlled search_query parameter directly into SQL: s_query = f\"SELECT filename FROM files WHERE user_id = {user_id} AND filename LIKE '%{search_query}%'\". The search_query comes from reques" + +[[pair]] +name = "vampi-books-get-by-title" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "erev0s/VAmPI" +cve = "" +cwe = "CWE-639" +commit = "1713b54b601ad29582581eeda4b31fceb1319874" +parent = "1713b54b601ad29582581eeda4b31fceb1319874" +commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51" +vuln = "vampi-books-get-by-title/vuln.py" +fixed = "vampi-books-get-by-title/fixed.py" +relpath = "api_views/books.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-639" +class = "broken_access_control" +path = "api_views/books.py" +function = "get_by_title" +mechanism = "missing_auth_guard" +evidence = "Broken Object Level Authorization (BOLA). When vuln=1, get_by_title queries the book by title only (Book.query.filter_by(book_title=str(book_title)).first()) without verifying the authenticated user is the owner. Any authenticated user can " + +[[pair]] +name = "vampi-user-model-get-user" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "erev0s/VAmPI" +cve = "" +cwe = "CWE-89" +commit = "1713b54b601ad29582581eeda4b31fceb1319874" +parent = "1713b54b601ad29582581eeda4b31fceb1319874" +commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/models/user_model.py#L72" +vuln = "vampi-user-model-get-user/vuln.py" +fixed = "vampi-user-model-get-user/fixed.py" +relpath = "models/user_model.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "models/user_model.py" +function = "get_user" +mechanism = "source_reaches_sink" +evidence = "SQL injection via f-string interpolation of unsanitized username path parameter into raw SQL query: user_query = f\"SELECT * FROM users WHERE username = '{username}'\" followed by db.session.execute(text(user_query)). Reachable from GET /user" + +[[pair]] +name = "vampi-users-update-password" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "erev0s/VAmPI" +cve = "" +cwe = "CWE-639" +commit = "1713b54b601ad29582581eeda4b31fceb1319874" +parent = "1713b54b601ad29582581eeda4b31fceb1319874" +commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187" +vuln = "vampi-users-update-password/vuln.py" +fixed = "vampi-users-update-password/fixed.py" +relpath = "api_views/users.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-639" +class = "broken_access_control" +path = "api_views/users.py" +function = "update_password" +mechanism = "missing_auth_guard" +evidence = "Unauthorized password change (IDOR). When vuln=1, the update_password function uses the username from the URL path parameter to look up the user whose password to change, instead of using the authenticated user from the JWT token (resp['sub" + +[[pair]] +name = "vfapi-main-delete-user" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "naryal2580/vfapi" +cve = "" +cwe = "CWE-89" +commit = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +parent = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +commit_url = "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L231" +vuln = "vfapi-main-delete-user/vuln.py" +fixed = "vfapi-main-delete-user/fixed.py" +relpath = "main.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "main.py" +function = "delete_user" +mechanism = "source_reaches_sink" +evidence = "SQL injection via f-string interpolation of username query parameter into DELETE SQL query: f'DELETE FROM users WHERE username = \"{username}\";'. No sanitization or parameterization." + +[[pair]] +name = "vfapi-main-put-user" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "naryal2580/vfapi" +cve = "" +cwe = "CWE-89" +commit = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +parent = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +commit_url = "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L188" +vuln = "vfapi-main-put-user/vuln.py" +fixed = "vfapi-main-put-user/fixed.py" +relpath = "main.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "main.py" +function = "put_user" +mechanism = "source_reaches_sink" +evidence = "SQL injection via f-string interpolation of all user-supplied fields (name, username, password, address, email, contact) into an INSERT SQL query. All fields from the User model body are directly embedded in the query string without paramet" + +[[pair]] +name = "vfapi-main-sql-return-users-from-username" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "naryal2580/vfapi" +cve = "" +cwe = "CWE-89" +commit = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +parent = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +commit_url = "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L182" +vuln = "vfapi-main-sql-return-users-from-username/vuln.py" +fixed = "vfapi-main-sql-return-users-from-username/fixed.py" +relpath = "main.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "main.py" +function = "sql_return_users_from_username" +mechanism = "source_reaches_sink" +evidence = "SQL injection via f-string interpolation of username query parameter directly into SQL query: f'SELECT * FROM users WHERE username = \"{username}\";'. No sanitization or parameterization." + +[[pair]] +name = "vulnerable-python-apps-bad-mvc-do-get" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "GPL-3.0" +repo = "LinuxUser255/Vulnerable_Python_Apps" +cve = "" +cwe = "CWE-79" +commit = "6336c77e6db523a5df34c4b628dff4159110797a" +parent = "6336c77e6db523a5df34c4b628dff4159110797a" +commit_url = "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Insecure_Frameworks/bad_mvc.py#L37" +vuln = "vulnerable-python-apps-bad-mvc-do-get/vuln.py" +fixed = "vulnerable-python-apps-bad-mvc-do-get/fixed.py" +relpath = "Insecure_Frameworks/bad_mvc.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-79" +class = "reflected_xss" +path = "Insecure_Frameworks/bad_mvc.py" +function = "do_GET" +mechanism = "source_reaches_sink" +evidence = "XSS via unescaped output. The username variable (derived from decrypting a user-supplied cookie at line 26) is rendered into HTML without any escaping: bytes('Hello %s' % username, 'utf-8'). An attacker who controls the encrypted cookie val" + +[[pair]] +name = "vulnerable-python-apps-dir-traversal-do-get" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "GPL-3.0" +repo = "LinuxUser255/Vulnerable_Python_Apps" +cve = "" +cwe = "CWE-22" +commit = "6336c77e6db523a5df34c4b628dff4159110797a" +parent = "6336c77e6db523a5df34c4b628dff4159110797a" +commit_url = "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Directory_Traversal/Dir_Traversal.py#L22" +vuln = "vulnerable-python-apps-dir-traversal-do-get/vuln.py" +fixed = "vulnerable-python-apps-dir-traversal-do-get/fixed.py" +relpath = "Directory_Traversal/Dir_Traversal.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-22" +class = "path_traversal" +path = "Directory_Traversal/Dir_Traversal.py" +function = "do_GET" +mechanism = "path_join_user_input" +evidence = "Arbitrary file read via path traversal. self.path from the HTTP request is concatenated to cwd at line 14 and opened at line 22 via open(path).read(). The regex filter on line 10-11 (r'/\\.\\.\\/\\.\\./' with re.match) is trivially bypassable — " + +[[pair]] +name = "vulnpy-cmdi-do-os-system" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "Contrast-Security-OSS/vulnpy" +cve = "" +cwe = "CWE-78" +commit = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +parent = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +commit_url = "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L6" +vuln = "vulnpy-cmdi-do-os-system/vuln.py" +fixed = "vulnpy-cmdi-do-os-system/fixed.py" +relpath = "src/vulnpy/trigger/cmdi.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-78" +class = "command_injection" +path = "src/vulnpy/trigger/cmdi.py" +function = "do_os_system" +mechanism = "source_reaches_sink" +evidence = "User-controlled command passed directly to os.system(command) with no sanitization, enabling arbitrary OS command execution" + +[[pair]] +name = "vulnpy-cmdi-do-subprocess-popen" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "Contrast-Security-OSS/vulnpy" +cve = "" +cwe = "CWE-78" +commit = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +parent = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +commit_url = "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L10" +vuln = "vulnpy-cmdi-do-subprocess-popen/vuln.py" +fixed = "vulnpy-cmdi-do-subprocess-popen/fixed.py" +relpath = "src/vulnpy/trigger/cmdi.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-78" +class = "command_injection" +path = "src/vulnpy/trigger/cmdi.py" +function = "do_subprocess_popen" +mechanism = "source_reaches_sink" +evidence = "User-controlled command passed to subprocess.Popen with shell=True, enabling arbitrary OS command execution" + +[[pair]] +name = "vulnpy-deserialization-do-pickle-load" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "Contrast-Security-OSS/vulnpy" +cve = "" +cwe = "CWE-502" +commit = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +parent = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +commit_url = "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/deserialization.py#L8" +vuln = "vulnpy-deserialization-do-pickle-load/vuln.py" +fixed = "vulnpy-deserialization-do-pickle-load/fixed.py" +relpath = "src/vulnpy/trigger/deserialization.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-502" +class = "insecure_deserialization" +path = "src/vulnpy/trigger/deserialization.py" +function = "do_pickle_load" +mechanism = "source_reaches_sink" +evidence = "User input encoded to bytes and passed to pickle.load(), which can execute arbitrary code via crafted pickle payloads" + +[[pair]] +name = "vulpy-libuser-create" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "fportantier/vulpy" +cve = "" +cwe = "CWE-89" +commit = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +parent = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +commit_url = "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L25" +vuln = "vulpy-libuser-create/vuln.py" +fixed = "vulpy-libuser-create/fixed.py" +relpath = "bad/libuser.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "bad/libuser.py" +function = "create" +mechanism = "source_reaches_sink" +evidence = "SQL injection via %-formatting in INSERT statement: c.execute(\"INSERT INTO users ... VALUES ('%s', '%s', ...\" %(username, password, ...)). User-supplied username and password are interpolated via % string formatting into the SQL query." + +[[pair]] +name = "vulpy-libuser-login" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "fportantier/vulpy" +cve = "" +cwe = "CWE-89" +commit = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +parent = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +commit_url = "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L12" +vuln = "vulpy-libuser-login/vuln.py" +fixed = "vulpy-libuser-login/fixed.py" +relpath = "bad/libuser.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "train" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "bad/libuser.py" +function = "login" +mechanism = "source_reaches_sink" +evidence = "SQL injection via string format in login query: c.execute(\"SELECT * FROM users WHERE username = '{}' and password = '{}'\".format(username, password)). User-supplied username and password from request.form are directly interpolated into the " + +[[pair]] +name = "vulpy-libuser-password-change" +slice = "vibe-py" +language = "python" +origin = "real-vuln-benchmark" +license = "MIT" +repo = "fportantier/vulpy" +cve = "" +cwe = "CWE-89" +commit = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +parent = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +commit_url = "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L53" +vuln = "vulpy-libuser-password-change/vuln.py" +fixed = "vulpy-libuser-password-change/fixed.py" +relpath = "bad/libuser.py" +min_recall = 1.0 +fix_policy = "silent" +provenance = "human" +split = "holdout" + +[[pair.expected]] +cwe = "CWE-89" +class = "sql_injection" +path = "bad/libuser.py" +function = "password_change" +mechanism = "source_reaches_sink" +evidence = "SQL injection via string format in UPDATE statement: c.execute(\"UPDATE users SET password = '{}' WHERE username = '{}'\".format(password, username)). The password parameter from user input is directly interpolated into the SQL query." diff --git a/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-mutate/fixed.py b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-mutate/fixed.py new file mode 100644 index 0000000..ea0a154 --- /dev/null +++ b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-mutate/fixed.py @@ -0,0 +1,21 @@ +# Provenance: dolevf/Damn-Vulnerable-GraphQL-Application mutate (fixed). +# repo: dolevf/Damn-Vulnerable-GraphQL-Application +# commit: a961308c02d1fb462b192681c336b0739e432da7 +# parent: a961308c02d1fb462b192681c336b0739e432da7 +# commit_url: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L211 +# cve: +# license: MIT +# function: mutate +# relpath: core/views.py +# provenance: human +# mechanism: source_reaches_sink + + def mutate(self, info , username, password) : + user = User.query.filter_by(username=username, password=password).first() + Audit.create_audit_entry(info) + if not user: + raise Exception('Authentication Failure') + return Login( + access_token = create_access_token(username), + refresh_token = create_refresh_token(username) + ) diff --git a/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-mutate/vuln.py b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-mutate/vuln.py new file mode 100644 index 0000000..8ae4995 --- /dev/null +++ b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-mutate/vuln.py @@ -0,0 +1,27 @@ +# Provenance: dolevf/Damn-Vulnerable-GraphQL-Application mutate (vuln). +# repo: dolevf/Damn-Vulnerable-GraphQL-Application +# commit: a961308c02d1fb462b192681c336b0739e432da7 +# parent: a961308c02d1fb462b192681c336b0739e432da7 +# commit_url: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L211 +# cve: +# license: MIT +# function: mutate +# relpath: core/views.py +# provenance: human +# mechanism: source_reaches_sink + + def mutate(self, info, host='pastebin.com', port=443, path='/', scheme="http"): + url = security.strip_dangerous_characters(f"{scheme}://{host}:{port}{path}") + cmd = helpers.run_cmd(f'curl --insecure {url}') + + owner = Owner.query.filter_by(name='DVGAUser').first() + Paste.create_paste( + title='Imported Paste from URL - {}'.format(helpers.generate_uuid()), + content=cmd, public=False, burn=False, + owner_id=owner.id, owner=owner, ip_addr=request.remote_addr, + user_agent=request.headers.get('User-Agent', '') + ) + + Audit.create_audit_entry(info) + + return ImportPaste(result=cmd) diff --git a/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-pastes/fixed.py b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-pastes/fixed.py new file mode 100644 index 0000000..0feeb9f --- /dev/null +++ b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-pastes/fixed.py @@ -0,0 +1,21 @@ +# Provenance: dolevf/Damn-Vulnerable-GraphQL-Application resolve_pastes (fixed). +# repo: dolevf/Damn-Vulnerable-GraphQL-Application +# commit: a961308c02d1fb462b192681c336b0739e432da7 +# parent: a961308c02d1fb462b192681c336b0739e432da7 +# commit_url: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L320 +# cve: +# license: MIT +# function: resolve_pastes +# relpath: core/views.py +# provenance: human +# mechanism: source_reaches_sink + + def mutate(self, info , username, password) : + user = User.query.filter_by(username=username, password=password).first() + Audit.create_audit_entry(info) + if not user: + raise Exception('Authentication Failure') + return Login( + access_token = create_access_token(username), + refresh_token = create_refresh_token(username) + ) diff --git a/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-pastes/vuln.py b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-pastes/vuln.py new file mode 100644 index 0000000..1fb0569 --- /dev/null +++ b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-pastes/vuln.py @@ -0,0 +1,21 @@ +# Provenance: dolevf/Damn-Vulnerable-GraphQL-Application resolve_pastes (vuln). +# repo: dolevf/Damn-Vulnerable-GraphQL-Application +# commit: a961308c02d1fb462b192681c336b0739e432da7 +# parent: a961308c02d1fb462b192681c336b0739e432da7 +# commit_url: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L320 +# cve: +# license: MIT +# function: resolve_pastes +# relpath: core/views.py +# provenance: human +# mechanism: source_reaches_sink + + def resolve_pastes(self, info, public=False, limit=1000, filter=None): + query = PasteObject.get_query(info) + Audit.create_audit_entry(info) + result = query.filter_by(public=public, burn=False) + + if filter: + result = result.filter(text("title = '%s' or content = '%s'" % (filter, filter))) + + return result.order_by(Paste.id.desc()).limit(limit) diff --git a/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-system-dia/fixed.py b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-system-dia/fixed.py new file mode 100644 index 0000000..163c97d --- /dev/null +++ b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-system-dia/fixed.py @@ -0,0 +1,21 @@ +# Provenance: dolevf/Damn-Vulnerable-GraphQL-Application resolve_system_diagnostics (fixed). +# repo: dolevf/Damn-Vulnerable-GraphQL-Application +# commit: a961308c02d1fb462b192681c336b0739e432da7 +# parent: a961308c02d1fb462b192681c336b0739e432da7 +# commit_url: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L345 +# cve: +# license: MIT +# function: resolve_system_diagnostics +# relpath: core/views.py +# provenance: human +# mechanism: source_reaches_sink + + def mutate(self, info , username, password) : + user = User.query.filter_by(username=username, password=password).first() + Audit.create_audit_entry(info) + if not user: + raise Exception('Authentication Failure') + return Login( + access_token = create_access_token(username), + refresh_token = create_refresh_token(username) + ) diff --git a/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-system-dia/vuln.py b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-system-dia/vuln.py new file mode 100644 index 0000000..e799f50 --- /dev/null +++ b/benchmarks/pairs/vibe-py/damn-vulnerable-graphql-application-views-resolve-system-dia/vuln.py @@ -0,0 +1,23 @@ +# Provenance: dolevf/Damn-Vulnerable-GraphQL-Application resolve_system_diagnostics (vuln). +# repo: dolevf/Damn-Vulnerable-GraphQL-Application +# commit: a961308c02d1fb462b192681c336b0739e432da7 +# parent: a961308c02d1fb462b192681c336b0739e432da7 +# commit_url: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L345 +# cve: +# license: MIT +# function: resolve_system_diagnostics +# relpath: core/views.py +# provenance: human +# mechanism: source_reaches_sink + + def resolve_system_diagnostics(self, info, username, password, cmd='whoami'): + q = User.query.filter_by(username='admin').first() + real_passw = q.password + res, msg = security.check_creds(username, password, real_passw) + Audit.create_audit_entry(info) + if res: + output = f'{cmd}: command not found' + if security.allowed_cmds(cmd): + output = helpers.run_cmd(cmd) + return output + return msg diff --git a/benchmarks/pairs/vibe-py/djangoat-analytics-hits-by-ip/fixed.py b/benchmarks/pairs/vibe-py/djangoat-analytics-hits-by-ip/fixed.py new file mode 100644 index 0000000..b812379 --- /dev/null +++ b/benchmarks/pairs/vibe-py/djangoat-analytics-hits-by-ip/fixed.py @@ -0,0 +1,18 @@ +# Provenance: Contrast-Security-OSS/DjanGoat hits_by_ip (fixed). +# repo: Contrast-Security-OSS/DjanGoat +# commit: 082c88bfefced508ae083b92b459c808f95e905e +# parent: 082c88bfefced508ae083b92b459c808f95e905e +# commit_url: https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/models/Analytics/analytics.py#L57 +# cve: +# license: MIT +# function: hits_by_ip +# relpath: app/models/Analytics/analytics.py +# provenance: human +# mechanism: source_reaches_sink + + def parse_field(field): + valid_fields = ["ip_address", "referrer", "user_agent"] + if field in valid_fields: + return field + else: + return '1' diff --git a/benchmarks/pairs/vibe-py/djangoat-analytics-hits-by-ip/vuln.py b/benchmarks/pairs/vibe-py/djangoat-analytics-hits-by-ip/vuln.py new file mode 100644 index 0000000..da33ad9 --- /dev/null +++ b/benchmarks/pairs/vibe-py/djangoat-analytics-hits-by-ip/vuln.py @@ -0,0 +1,21 @@ +# Provenance: Contrast-Security-OSS/DjanGoat hits_by_ip (vuln). +# repo: Contrast-Security-OSS/DjanGoat +# commit: 082c88bfefced508ae083b92b459c808f95e905e +# parent: 082c88bfefced508ae083b92b459c808f95e905e +# commit_url: https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/models/Analytics/analytics.py#L57 +# cve: +# license: MIT +# function: hits_by_ip +# relpath: app/models/Analytics/analytics.py +# provenance: human +# mechanism: source_reaches_sink + + def hits_by_ip(cls, ip, col='*'): + table_name = cls.objects.model._meta.db_table + cmd = "SELECT %s FROM %s WHERE ip_address='%s' ORDER BY id DESC" % ( + col, table_name, ip) + with connection.cursor() as cursor: + cursor.execute(cmd) + raw = cursor.fetchall() + formatted = Analytics.format_raw_sql(cmd, raw, col) + return formatted diff --git a/benchmarks/pairs/vibe-py/djangoat-views-reset-password/fixed.py b/benchmarks/pairs/vibe-py/djangoat-views-reset-password/fixed.py new file mode 100644 index 0000000..9ff8ed2 --- /dev/null +++ b/benchmarks/pairs/vibe-py/djangoat-views-reset-password/fixed.py @@ -0,0 +1,25 @@ +# Provenance: Contrast-Security-OSS/DjanGoat reset_password (fixed). +# repo: Contrast-Security-OSS/DjanGoat +# commit: 082c88bfefced508ae083b92b459c808f95e905e +# parent: 082c88bfefced508ae083b92b459c808f95e905e +# commit_url: https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/password_resets/views.py#L84 +# cve: +# license: MIT +# function: reset_password +# relpath: app/views/password_resets/views.py +# provenance: human +# mechanism: source_reaches_sink + +def forgot_password(request): + qset = User.objects.filter(email=request.POST.get('email', '')) + if len(qset) > 0: + user = qset.first() + messages.success(request, 'An email was sent to reset your password!') + password_reset_mailer(request, user) + else: + try: + messages.error(request, 'We do not have the email in our system') + except: + pass + + return redirect('/login') diff --git a/benchmarks/pairs/vibe-py/djangoat-views-reset-password/vuln.py b/benchmarks/pairs/vibe-py/djangoat-views-reset-password/vuln.py new file mode 100644 index 0000000..27d20e1 --- /dev/null +++ b/benchmarks/pairs/vibe-py/djangoat-views-reset-password/vuln.py @@ -0,0 +1,26 @@ +# Provenance: Contrast-Security-OSS/DjanGoat reset_password (vuln). +# repo: Contrast-Security-OSS/DjanGoat +# commit: 082c88bfefced508ae083b92b459c808f95e905e +# parent: 082c88bfefced508ae083b92b459c808f95e905e +# commit_url: https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/password_resets/views.py#L84 +# cve: +# license: MIT +# function: reset_password +# relpath: app/views/password_resets/views.py +# provenance: human +# mechanism: source_reaches_sink + +def reset_password(request): + if request.POST.get('user', '') != '': + encoded_user = request.POST['user'] + user = pickle.loads(base64.b64decode(encoded_user)) + user.password = request.POST['password'] + user.save() + messages.success(request, 'Your password has been updated') + else: + try: + messages.error(request, 'Password did not reset') + except: + pass + + return redirect('/login') diff --git a/benchmarks/pairs/vibe-py/djangoat-views-user-view/fixed.py b/benchmarks/pairs/vibe-py/djangoat-views-user-view/fixed.py new file mode 100644 index 0000000..d4b13db --- /dev/null +++ b/benchmarks/pairs/vibe-py/djangoat-views-user-view/fixed.py @@ -0,0 +1,18 @@ +# Provenance: Contrast-Security-OSS/DjanGoat user_view (fixed). +# repo: Contrast-Security-OSS/DjanGoat +# commit: 082c88bfefced508ae083b92b459c808f95e905e +# parent: 082c88bfefced508ae083b92b459c808f95e905e +# commit_url: https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/users/views.py#L74 +# cve: +# license: MIT +# function: user_view +# relpath: app/views/users/views.py +# provenance: human +# mechanism: source_reaches_sink + + def parse_field(field): + valid_fields = ["ip_address", "referrer", "user_agent"] + if field in valid_fields: + return field + else: + return '1' diff --git a/benchmarks/pairs/vibe-py/djangoat-views-user-view/vuln.py b/benchmarks/pairs/vibe-py/djangoat-views-user-view/vuln.py new file mode 100644 index 0000000..41c88db --- /dev/null +++ b/benchmarks/pairs/vibe-py/djangoat-views-user-view/vuln.py @@ -0,0 +1,49 @@ +# Provenance: Contrast-Security-OSS/DjanGoat user_view (vuln). +# repo: Contrast-Security-OSS/DjanGoat +# commit: 082c88bfefced508ae083b92b459c808f95e905e +# parent: 082c88bfefced508ae083b92b459c808f95e905e +# commit_url: https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/users/views.py#L74 +# cve: +# license: MIT +# function: user_view +# relpath: app/views/users/views.py +# provenance: human +# mechanism: source_reaches_sink + +def user_view(request, user_id): + if request.method == "POST": + form = request.POST + if not form: + return HttpResponse("User " + str(user_id) + "POST") + user_id_form = form['user_id'] + table_name = User.objects.model._meta.db_table + # The order by is_admin='0' moves admin to the first in list + # which allows sql injection + users = User.objects.raw( + "SELECT * FROM %s WHERE user_id='%s' ORDER BY is_admin='0'" + % (table_name, user_id_form)) + try: + user = users[0] + except: + return HttpResponse("User " + str(user_id_form) + " NOT FOUND") + update = dict() + err_msg = User.validate_update_form(form, user, update) + if len(err_msg) > 0: + messages.add_message(request, messages.INFO, err_msg) + else: + try: + # skip hash_password if password not updated + if "password" not in update: + User.objects.filter(pk=user.pk).update(**update) + else: + user.__dict__.update(update) + user.save() + messages.add_message(request, messages.INFO, + "Successfully Updated") + except Exception as e: + messages.add_message(request, messages.INFO, str(e)) + return redirect("/users/%s/account_settings" % user_id, + permanent=False) + + else: + return HttpResponse("User " + str(user_id) + " " + str(request.method)) diff --git a/benchmarks/pairs/vibe-py/dsvpwa-attacks-run/fixed.py b/benchmarks/pairs/vibe-py/dsvpwa-attacks-run/fixed.py new file mode 100644 index 0000000..0d4f868 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvpwa-attacks-run/fixed.py @@ -0,0 +1,43 @@ +# Provenance: sgabe/DSVPWA run (fixed). +# repo: sgabe/DSVPWA +# commit: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# parent: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# commit_url: https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/attacks.py#L39 +# cve: +# license: MIT +# function: run +# relpath: dsvpwa/attacks.py +# provenance: human +# mechanism: source_reaches_sink + + def run(self, handler): + params = handler.params + connection = handler.server.connection + cursor = connection.cursor() + + if 'comment' in params: + comment = params.get('comment', '')[0] + cursor.execute('INSERT INTO comments VALUES(NULL, ?, ?)', [comment, time.ctime()]) + connection.commit() + content = 'Thank you for leaving the comment. Please click here to see all comments...' + else: + cursor.execute("SELECT id, comment, time FROM comments") + rows = "" + for row in cursor.fetchall(): + columns = "" + for column in row: + columns += "".join("{}".format("-" if column is None else column)) + rows += "".join("{}".format(columns)) + + content = ''' +
Comment(s):
+ + + + + + + {} +
idcommenttime
'''.format(rows) + + return content diff --git a/benchmarks/pairs/vibe-py/dsvpwa-attacks-run/vuln.py b/benchmarks/pairs/vibe-py/dsvpwa-attacks-run/vuln.py new file mode 100644 index 0000000..f04e846 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvpwa-attacks-run/vuln.py @@ -0,0 +1,44 @@ +# Provenance: sgabe/DSVPWA run (vuln). +# repo: sgabe/DSVPWA +# commit: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# parent: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# commit_url: https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/attacks.py#L39 +# cve: +# license: MIT +# function: run +# relpath: dsvpwa/attacks.py +# provenance: human +# mechanism: source_reaches_sink + + def run(self, handler): + params = handler.params + cursor = handler.server.connection.cursor() + + id = '9999999' if 'id' not in params else params['id'][0] + try: + cursor.execute("SELECT id, username, firstname, lastname, email, session FROM users WHERE id=" + id) + except sqlite3.OperationalError as e: + return e + + rows = "" + for row in cursor.fetchall(): + columns = "" + for column in row: + columns += "".join("{}".format("-" if column is None else column)) + rows += "".join("{}".format(columns)) + + content = """ + + + + + + + + + + {} +
IDUsernameFirst nameLast nameE-mail addressSession
+ """.format(rows) + + return content diff --git a/benchmarks/pairs/vibe-py/dsvpwa-dsvpwa-main/fixed.py b/benchmarks/pairs/vibe-py/dsvpwa-dsvpwa-main/fixed.py new file mode 100644 index 0000000..821e354 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvpwa-dsvpwa-main/fixed.py @@ -0,0 +1,43 @@ +# Provenance: sgabe/DSVPWA main (fixed). +# repo: sgabe/DSVPWA +# commit: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# parent: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# commit_url: https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa.py#L37 +# cve: +# license: MIT +# function: main +# relpath: dsvpwa.py +# provenance: human +# mechanism: permissive_default + + def run(self, handler): + params = handler.params + connection = handler.server.connection + cursor = connection.cursor() + + if 'comment' in params: + comment = params.get('comment', '')[0] + cursor.execute('INSERT INTO comments VALUES(NULL, ?, ?)', [comment, time.ctime()]) + connection.commit() + content = 'Thank you for leaving the comment. Please click here to see all comments...' + else: + cursor.execute("SELECT id, comment, time FROM comments") + rows = "" + for row in cursor.fetchall(): + columns = "" + for column in row: + columns += "".join("{}".format("-" if column is None else column)) + rows += "".join("{}".format(columns)) + + content = ''' +
Comment(s):
+ + + + + + + {} +
idcommenttime
'''.format(rows) + + return content diff --git a/benchmarks/pairs/vibe-py/dsvpwa-dsvpwa-main/vuln.py b/benchmarks/pairs/vibe-py/dsvpwa-dsvpwa-main/vuln.py new file mode 100644 index 0000000..7743e27 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvpwa-dsvpwa-main/vuln.py @@ -0,0 +1,52 @@ +# Provenance: sgabe/DSVPWA main (vuln). +# repo: sgabe/DSVPWA +# commit: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# parent: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# commit_url: https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa.py#L37 +# cve: +# license: MIT +# function: main +# relpath: dsvpwa.py +# provenance: human +# mechanism: permissive_default + +def main(): + + parser = argparse.ArgumentParser(prog='DSVPWA', + description='Damn Simple Vulnerable Python Web Application') + parser.add_argument('--host', default='127.0.0.1', + help='set the IP address to bind to (defaults to 127.0.0.1)') + parser.add_argument('--port', type=int, default=os.getenv('DSVPWA_PORT', 65413), + help='set the port number to bind to (defaults to 65413)') + parser.add_argument('--risk', type=int, default=os.getenv('DSVPWA_RISK', 1), choices=range(1,4), + help='set the risk level in the range 1-3') + parser.add_argument('--ssl', action='store_true', default=os.getenv('DSVPWA_SSL', 0), + help='enable encryption (defaults to false)') + parser.add_argument('--version', action='version', + version='%(prog)s v{} ({})'.format(BUILD_VER, BUILD_REV)) + + args = parser.parse_args() + proto = 'http' if not args.ssl else 'https' + + try: + httpd = VulnHTTPServer((args.host, args.port), VulnHTTPRequestHandler) + httpd.RequestHandlerClass.risk = args.risk + + if args.ssl: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.options &= ~ssl.OP_NO_SSLv3 + ctx.options &= ~ssl.OP_NO_COMPRESSION + ctx.options &= ~ssl.OP_CIPHER_SERVER_PREFERENCE + ctx.load_cert_chain(certfile='./ssl/cert.pem', keyfile='./ssl/key.pem') + httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) + + print('[*] Navigate to {}://{}:{} to access DSVPWA'.format(proto, args.host, args.port)) + httpd.serve_forever() + except KeyboardInterrupt: + print('[*] Quitting...') + pass + except Exception as ex: + print("[!] Exception occurred ('%s')" % ex) + finally: + httpd.server_close() + os._exit(0) diff --git a/benchmarks/pairs/vibe-py/dsvpwa-handlers-do-bdr/fixed.py b/benchmarks/pairs/vibe-py/dsvpwa-handlers-do-bdr/fixed.py new file mode 100644 index 0000000..2380d52 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvpwa-handlers-do-bdr/fixed.py @@ -0,0 +1,43 @@ +# Provenance: sgabe/DSVPWA do_BDR (fixed). +# repo: sgabe/DSVPWA +# commit: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# parent: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# commit_url: https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/handlers.py#L183 +# cve: +# license: MIT +# function: do_BDR +# relpath: dsvpwa/handlers.py +# provenance: human +# mechanism: source_reaches_sink + + def run(self, handler): + params = handler.params + connection = handler.server.connection + cursor = connection.cursor() + + if 'comment' in params: + comment = params.get('comment', '')[0] + cursor.execute('INSERT INTO comments VALUES(NULL, ?, ?)', [comment, time.ctime()]) + connection.commit() + content = 'Thank you for leaving the comment. Please click here to see all comments...' + else: + cursor.execute("SELECT id, comment, time FROM comments") + rows = "" + for row in cursor.fetchall(): + columns = "" + for column in row: + columns += "".join("{}".format("-" if column is None else column)) + rows += "".join("{}".format(columns)) + + content = ''' +
Comment(s):
+ + + + + + + {} +
idcommenttime
'''.format(rows) + + return content diff --git a/benchmarks/pairs/vibe-py/dsvpwa-handlers-do-bdr/vuln.py b/benchmarks/pairs/vibe-py/dsvpwa-handlers-do-bdr/vuln.py new file mode 100644 index 0000000..1a7b336 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvpwa-handlers-do-bdr/vuln.py @@ -0,0 +1,30 @@ +# Provenance: sgabe/DSVPWA do_BDR (vuln). +# repo: sgabe/DSVPWA +# commit: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# parent: a06abb306ed3699bb2f073e3f41d3d9c57012819 +# commit_url: https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/handlers.py#L183 +# cve: +# license: MIT +# function: do_BDR +# relpath: dsvpwa/handlers.py +# provenance: human +# mechanism: source_reaches_sink + + def do_BDR(self): + if self.risk < 3: + self.send_response(HTTPStatus.BAD_REQUEST) + content = dsvpwa.attacks.Attack.warning.format(self.risk).encode() + else: + self.send_response(HTTPStatus.OK) + content = subprocess.check_output( + self.path[1:], + shell=True, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE + ) + + self.send_header('Content-type', 'text/plain') + self.send_header('Connection', 'close') + self.end_headers() + self.wfile.write(content) + self.wfile.flush() diff --git a/benchmarks/pairs/vibe-py/dsvw-dsvw-do-get/fixed.py b/benchmarks/pairs/vibe-py/dsvw-dsvw-do-get/fixed.py new file mode 100644 index 0000000..4fe851d --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvw-dsvw-do-get/fixed.py @@ -0,0 +1,70 @@ +# Provenance: stamparm/DSVW do_GET (fixed). +# repo: stamparm/DSVW +# commit: 7d40f4b7939c901610ed9b85724552d60e7d63fa +# parent: 7d40f4b7939c901610ed9b85724552d60e7d63fa +# commit_url: https://github.com/stamparm/DSVW/blob/7d40f4b7939c901610ed9b85724552d60e7d63fa/dsvw.py#L30 +# cve: +# license: Unlicense +# function: do_GET +# relpath: dsvw.py +# provenance: human +# mechanism: source_reaches_sink + + def do_GET(self): + path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "") + code, content, params, cursor = http.client.OK, HTML_PREFIX, dict((match.group("parameter"), urllib.parse.unquote(','.join(re.findall(r"(?:\A|[?&])%s=([^&]+)" % match.group("parameter"), query)))) for match in re.finditer(r"((\A|[?&])(?P[\w\[\]]+)=)([^&]+)", query)), connection.cursor() + try: + if path == '/': + if "id" in params: + cursor.execute("SELECT id, username, name, surname FROM users WHERE id=" + params["id"]) + content += "
Result(s):
%s
idusernamenamesurname
%s" % ("".join("%s" % "".join("%s" % ("-" if _ is None else _) for _ in row) for row in cursor.fetchall()), HTML_POSTFIX) + elif "v" in params: + content += re.sub(r"(v)[^<]+()", r"\g<1>%s\g<2>" % params["v"], HTML_POSTFIX) + elif "object" in params: + content = str(pickle.loads(params["object"].encode())) + elif "path" in params: + content = (open(os.path.abspath(params["path"]), "rb") if not "://" in params["path"] else urllib.request.urlopen(params["path"])).read().decode() + elif "domain" in params: + content = subprocess.run("nslookup " + params["domain"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, text=True).stdout + elif "xml" in params: + content = lxml.etree.tostring(lxml.etree.parse(io.BytesIO(params["xml"].encode()), lxml.etree.XMLParser(load_dtd=True, resolve_entities=True, no_network=False)), pretty_print=True).decode() + elif "name" in params: + found = lxml.etree.parse(io.BytesIO(USERS_XML.encode())).xpath(".//user[name/text()='%s']" % params["name"]) + content += "Surname: %s%s" % (found[-1].find("surname").text if found else "-", HTML_POSTFIX) + elif "size" in params: + start, _ = time.time(), "
".join("#" * int(params["size"]) for _ in range(int(params["size"]))) + content += "Time required (to 'resize image' to %dx%d): %.6f seconds%s" % (int(params["size"]), int(params["size"]), time.time() - start, HTML_POSTFIX) + elif "comment" in params or query == "comment=": + if "comment" in params: + cursor.execute("INSERT INTO comments VALUES(NULL, '%s', '%s')" % (params["comment"], time.ctime())) + content += "Thank you for leaving the comment. Please click here here to see all comments%s" % HTML_POSTFIX + else: + cursor.execute("SELECT id, comment, time FROM comments") + content += "
Comment(s):
%s
idcommenttime
%s" % ("".join("%s" % "".join("%s" % ("-" if _ is None else _) for _ in row) for row in cursor.fetchall()), HTML_POSTFIX) + elif "include" in params: + backup, sys.stdout, program, envs = sys.stdout, io.StringIO(), (open(params["include"], "rb") if not "://" in params["include"] else urllib.request.urlopen(params["include"])).read(), {"DOCUMENT_ROOT": os.getcwd(), "HTTP_USER_AGENT": self.headers.get("User-Agent"), "REMOTE_ADDR": self.client_address[0], "REMOTE_PORT": self.client_address[1], "PATH": path, "QUERY_STRING": query} + exec(program, envs) + content += sys.stdout.getvalue() + sys.stdout = backup + elif "redir" in params: + content = content.replace("", "" % params["redir"]) + if HTML_PREFIX in content and HTML_POSTFIX not in content: + content += "
Attacks:
\n
    %s\n
\n" % ("".join("\n%s - vulnerable|exploit|info" % (" class=\"disabled\" title=\"module 'python-lxml' not installed\"" if ("lxml.etree" not in sys.modules and any(_ in case[0].upper() for _ in ("XML", "XPATH"))) else "", case[0], case[1], case[2], case[3]) for case in CASES)).replace("vulnerable|", "-|") + elif path == "/users.json": + content = "%s%s%s" % ("" if not "callback" in params else "%s(" % params["callback"], json.dumps(dict((_.findtext("username"), _.findtext("surname")) for _ in xml.etree.ElementTree.fromstring(USERS_XML).findall("user"))), "" if not "callback" in params else ")") + elif path == "/login": + cursor.execute("SELECT * FROM users WHERE username='" + re.sub(r"[^\w]", "", params.get("username", "")) + "' AND password='" + params.get("password", "") + "'") + content += "Welcome %s" % (re.sub(r"[^\w]", "", params.get("username", "")), "".join(random.sample(string.ascii_letters + string.digits, 20))) if cursor.fetchall() else "The username and/or password is incorrect" + else: + code = http.client.NOT_FOUND + except Exception as ex: + content = ex.output if isinstance(ex, subprocess.CalledProcessError) else traceback.format_exc() + code = http.client.INTERNAL_SERVER_ERROR + finally: + self.send_response(code) + self.send_header("Connection", "close") + self.send_header("X-XSS-Protection", "0") + self.send_header("Content-Type", "%s%s" % ("text/html" if content.startswith("") else "text/plain", "; charset=%s" % params.get("charset", "utf8"))) + self.end_headers() + self.wfile.write(("%s%s" % (content, HTML_POSTFIX if HTML_PREFIX in content and GITHUB not in content else "")).encode()) + self.wfile.flush() diff --git a/benchmarks/pairs/vibe-py/dsvw-dsvw-do-get/vuln.py b/benchmarks/pairs/vibe-py/dsvw-dsvw-do-get/vuln.py new file mode 100644 index 0000000..422311c --- /dev/null +++ b/benchmarks/pairs/vibe-py/dsvw-dsvw-do-get/vuln.py @@ -0,0 +1,70 @@ +# Provenance: stamparm/DSVW do_GET (vuln). +# repo: stamparm/DSVW +# commit: 7d40f4b7939c901610ed9b85724552d60e7d63fa +# parent: 7d40f4b7939c901610ed9b85724552d60e7d63fa +# commit_url: https://github.com/stamparm/DSVW/blob/7d40f4b7939c901610ed9b85724552d60e7d63fa/dsvw.py#L30 +# cve: +# license: Unlicense +# function: do_GET +# relpath: dsvw.py +# provenance: human +# mechanism: source_reaches_sink + + def do_GET(self): + path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "") + code, content, params, cursor = http.client.OK, HTML_PREFIX, dict((match.group("parameter"), urllib.parse.unquote(','.join(re.findall(r"(?:\A|[?&])%s=([^&]+)" % match.group("parameter"), query)))) for match in re.finditer(r"((\A|[?&])(?P[\w\[\]]+)=)([^&]+)", query)), connection.cursor() + try: + if path == '/': + if "id" in params: + cursor.execute("SELECT id, username, name, surname FROM users WHERE id=" + params["id"]) + content += "
Result(s):
%s
idusernamenamesurname
%s" % ("".join("%s" % "".join("%s" % ("-" if _ is None else _) for _ in row) for row in cursor.fetchall()), HTML_POSTFIX) + elif "v" in params: + content += re.sub(r"(v)[^<]+()", r"\g<1>%s\g<2>" % params["v"], HTML_POSTFIX) + elif "object" in params: + content = str(pickle.loads(params["object"].encode())) + elif "path" in params: + content = (open(os.path.abspath(params["path"]), "rb") if not "://" in params["path"] else urllib.request.urlopen(params["path"])).read().decode() + elif "domain" in params: + content = subprocess.run("nslookup " + params["domain"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, text=True).stdout + elif "xml" in params: + content = lxml.etree.tostring(lxml.etree.parse(io.BytesIO(params["xml"].encode()), lxml.etree.XMLParser(load_dtd=True, resolve_entities=True, no_network=False)), pretty_print=True).decode() + elif "name" in params: + found = lxml.etree.parse(io.BytesIO(USERS_XML.encode())).xpath(".//user[name/text()='%s']" % params["name"]) + content += "Surname: %s%s" % (found[-1].find("surname").text if found else "-", HTML_POSTFIX) + elif "size" in params: + start, _ = time.time(), "
".join("#" * int(params["size"]) for _ in range(int(params["size"]))) + content += "Time required (to 'resize image' to %dx%d): %.6f seconds%s" % (int(params["size"]), int(params["size"]), time.time() - start, HTML_POSTFIX) + elif "comment" in params or query == "comment=": + if "comment" in params: + cursor.execute("INSERT INTO comments VALUES(NULL, '%s', '%s')" % (params["comment"], time.ctime())) + content += "Thank you for leaving the comment. Please click here here to see all comments%s" % HTML_POSTFIX + else: + cursor.execute("SELECT id, comment, time FROM comments") + content += "
Comment(s):
%s
idcommenttime
%s" % ("".join("%s" % "".join("%s" % ("-" if _ is None else _) for _ in row) for row in cursor.fetchall()), HTML_POSTFIX) + elif "include" in params: + backup, sys.stdout, program, envs = sys.stdout, io.StringIO(), (open(params["include"], "rb") if not "://" in params["include"] else urllib.request.urlopen(params["include"])).read(), {"DOCUMENT_ROOT": os.getcwd(), "HTTP_USER_AGENT": self.headers.get("User-Agent"), "REMOTE_ADDR": self.client_address[0], "REMOTE_PORT": self.client_address[1], "PATH": path, "QUERY_STRING": query} + exec(program, envs) + content += sys.stdout.getvalue() + sys.stdout = backup + elif "redir" in params: + content = content.replace("", "" % params["redir"]) + if HTML_PREFIX in content and HTML_POSTFIX not in content: + content += "
Attacks:
\n
    %s\n
\n" % ("".join("\n%s - vulnerable|exploit|info" % (" class=\"disabled\" title=\"module 'python-lxml' not installed\"" if ("lxml.etree" not in sys.modules and any(_ in case[0].upper() for _ in ("XML", "XPATH"))) else "", case[0], case[1], case[2], case[3]) for case in CASES)).replace("vulnerable|", "-|") + elif path == "/users.json": + content = "%s%s%s" % ("" if not "callback" in params else "%s(" % params["callback"], json.dumps(dict((_.findtext("username"), _.findtext("surname")) for _ in xml.etree.ElementTree.fromstring(USERS_XML).findall("user"))), "" if not "callback" in params else ")") + elif path == "/login": + cursor.execute("SELECT * FROM users WHERE username='" + re.sub(r"[^\w]", "", params.get("username", "")) + "' AND password='" + params.get("password", "") + "'") + content += "Welcome %s" % (re.sub(r"[^\w]", "", params.get("username", "")), "".join(random.sample(string.ascii_letters + string.digits, 20))) if cursor.fetchall() else "The username and/or password is incorrect" + else: + code = http.client.NOT_FOUND + except Exception as ex: + content = ex.output if isinstance(ex, subprocess.CalledProcessError) else traceback.format_exc() + code = http.client.INTERNAL_SERVER_ERROR + finally: + self.send_response(code) + self.send_header("Connection", "close") + self.send_header("X-XSS-Protection", "0") + self.send_header("Content-Type", "%s%s" % ("text/html" if content.startswith("") else "text/plain", "; charset=%s" % params.get("charset", "utf8"))) + self.end_headers() + self.wfile.write(("%s%s" % (content, HTML_POSTFIX if HTML_PREFIX in content and GITHUB not in content else "")).encode()) + self.wfile.flush() diff --git a/benchmarks/pairs/vibe-py/dvpwa-app-init/fixed.py b/benchmarks/pairs/vibe-py/dvpwa-app-init/fixed.py new file mode 100644 index 0000000..432ff4f --- /dev/null +++ b/benchmarks/pairs/vibe-py/dvpwa-app-init/fixed.py @@ -0,0 +1,20 @@ +# Provenance: anxolerd/dvpwa init (fixed). +# repo: anxolerd/dvpwa +# commit: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# parent: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# commit_url: https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/app.py#L35 +# cve: +# license: MIT +# function: init +# relpath: sqli/app.py +# provenance: human +# mechanism: permissive_default + + async def get_by_username(conn: Connection, username: str): + async with conn.cursor() as cur: + await cur.execute( + 'SELECT id, first_name, middle_name, last_name, ' + 'username, pwd_hash, is_admin FROM users WHERE username = %s', + (username,), + ) + return User.from_raw(await cur.fetchone()) diff --git a/benchmarks/pairs/vibe-py/dvpwa-app-init/vuln.py b/benchmarks/pairs/vibe-py/dvpwa-app-init/vuln.py new file mode 100644 index 0000000..7ccffc4 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dvpwa-app-init/vuln.py @@ -0,0 +1,37 @@ +# Provenance: anxolerd/dvpwa init (vuln). +# repo: anxolerd/dvpwa +# commit: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# parent: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# commit_url: https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/app.py#L35 +# cve: +# license: MIT +# function: init +# relpath: sqli/app.py +# provenance: human +# mechanism: permissive_default + +def init(argv): + ap = ArgumentParser() + commandline.standard_argparse_options(ap, default_config='./config/dev.yaml') + options = ap.parse_args(argv) + + config = commandline.config_from_options(options, CONFIG_SCHEMA) + + app = Application( + debug=True, + middlewares=[ + session_middleware, + # csrf_middleware, + error_middleware, + ] + ) + app['config'] = config + + setup_jinja(app, loader=PackageLoader('sqli', 'templates'), + context_processors=[csrf_processor, auth_user_processor], + autoescape=False) + setup_database(app) + setup_redis(app) + setup_routes(app) + + return app diff --git a/benchmarks/pairs/vibe-py/dvpwa-middlewares-session-middleware/fixed.py b/benchmarks/pairs/vibe-py/dvpwa-middlewares-session-middleware/fixed.py new file mode 100644 index 0000000..f0a1a62 --- /dev/null +++ b/benchmarks/pairs/vibe-py/dvpwa-middlewares-session-middleware/fixed.py @@ -0,0 +1,20 @@ +# Provenance: anxolerd/dvpwa session_middleware (fixed). +# repo: anxolerd/dvpwa +# commit: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# parent: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# commit_url: https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/middlewares.py#L20 +# cve: +# license: MIT +# function: session_middleware +# relpath: sqli/middlewares.py +# provenance: human +# mechanism: permissive_default + + async def get_by_username(conn: Connection, username: str): + async with conn.cursor() as cur: + await cur.execute( + 'SELECT id, first_name, middle_name, last_name, ' + 'username, pwd_hash, is_admin FROM users WHERE username = %s', + (username,), + ) + return User.from_raw(await cur.fetchone()) diff --git a/benchmarks/pairs/vibe-py/dvpwa-middlewares-session-middleware/vuln.py b/benchmarks/pairs/vibe-py/dvpwa-middlewares-session-middleware/vuln.py new file mode 100644 index 0000000..5218e1b --- /dev/null +++ b/benchmarks/pairs/vibe-py/dvpwa-middlewares-session-middleware/vuln.py @@ -0,0 +1,22 @@ +# Provenance: anxolerd/dvpwa session_middleware (vuln). +# repo: anxolerd/dvpwa +# commit: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# parent: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# commit_url: https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/middlewares.py#L20 +# cve: +# license: MIT +# function: session_middleware +# relpath: sqli/middlewares.py +# provenance: human +# mechanism: permissive_default + +async def session_middleware(request, handler): + """Wrapper to Session Middleware factory. + """ + # Do the trick, by passing app & handler back to original session + # middleware factory. Do not forget to await on results here as original + # session middleware factory is also awaitable. + app = request.app + storage = RedisStorage(app['redis'], httponly=False) + middleware = session_middleware_(storage) + return await middleware(request, handler) diff --git a/benchmarks/pairs/vibe-py/dvpwa-student-create/fixed.py b/benchmarks/pairs/vibe-py/dvpwa-student-create/fixed.py new file mode 100644 index 0000000..70f190f --- /dev/null +++ b/benchmarks/pairs/vibe-py/dvpwa-student-create/fixed.py @@ -0,0 +1,20 @@ +# Provenance: anxolerd/dvpwa create (fixed). +# repo: anxolerd/dvpwa +# commit: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# parent: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# commit_url: https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/dao/student.py#L42 +# cve: +# license: MIT +# function: create +# relpath: sqli/dao/student.py +# provenance: human +# mechanism: source_reaches_sink + + async def get_by_username(conn: Connection, username: str): + async with conn.cursor() as cur: + await cur.execute( + 'SELECT id, first_name, middle_name, last_name, ' + 'username, pwd_hash, is_admin FROM users WHERE username = %s', + (username,), + ) + return User.from_raw(await cur.fetchone()) diff --git a/benchmarks/pairs/vibe-py/dvpwa-student-create/vuln.py b/benchmarks/pairs/vibe-py/dvpwa-student-create/vuln.py new file mode 100644 index 0000000..b2747dd --- /dev/null +++ b/benchmarks/pairs/vibe-py/dvpwa-student-create/vuln.py @@ -0,0 +1,17 @@ +# Provenance: anxolerd/dvpwa create (vuln). +# repo: anxolerd/dvpwa +# commit: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# parent: a1d8f89fac2e57093189853c6527c2b01fc1d9c1 +# commit_url: https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/dao/student.py#L42 +# cve: +# license: MIT +# function: create +# relpath: sqli/dao/student.py +# provenance: human +# mechanism: source_reaches_sink + + async def create(conn: Connection, name: str): + q = ("INSERT INTO students (name) " + "VALUES ('%(name)s')" % {'name': name}) + async with conn.cursor() as cur: + await cur.execute(q) diff --git a/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-before-request/fixed.py b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-before-request/fixed.py new file mode 100644 index 0000000..66064c7 --- /dev/null +++ b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-before-request/fixed.py @@ -0,0 +1,30 @@ +# Provenance: manuelz120/extremely-vulnerable-flask-app before_request (fixed). +# repo: manuelz120/extremely-vulnerable-flask-app +# commit: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# parent: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# commit_url: https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L118 +# cve: +# license: GPL-3.0 +# function: before_request +# relpath: routes/account.py +# provenance: human +# mechanism: source_reaches_sink + +def do_login(): + form = LoginForm(request.form) + + if not form.validate(): + flash(dumps(form.errors), 'error') + else: + with Session() as session: + user = session.query(User).filter( + User.email == form.email.data).first() + if user is not None and checkpw( + form.password.data.encode('utf-8'), + user.password.encode('utf-8')) and login_user(user): + return redirect("/") + + flash('Invalid Credentials!', 'warning') + logout_user() + + return redirect("/") diff --git a/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-before-request/vuln.py b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-before-request/vuln.py new file mode 100644 index 0000000..2e3206b --- /dev/null +++ b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-before-request/vuln.py @@ -0,0 +1,20 @@ +# Provenance: manuelz120/extremely-vulnerable-flask-app before_request (vuln). +# repo: manuelz120/extremely-vulnerable-flask-app +# commit: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# parent: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# commit_url: https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L118 +# cve: +# license: GPL-3.0 +# function: before_request +# relpath: routes/account.py +# provenance: human +# mechanism: source_reaches_sink + +def before_request(): + preferences = request.cookies.get('preferences') + if preferences is None: + preferences = default_preferences + else: + preferences = loads(b64decode(preferences)) + + g.preferences = preferences diff --git a/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-search/fixed.py b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-search/fixed.py new file mode 100644 index 0000000..75b7ecc --- /dev/null +++ b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-search/fixed.py @@ -0,0 +1,30 @@ +# Provenance: manuelz120/extremely-vulnerable-flask-app search (fixed). +# repo: manuelz120/extremely-vulnerable-flask-app +# commit: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# parent: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# commit_url: https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L33 +# cve: +# license: GPL-3.0 +# function: search +# relpath: routes/account.py +# provenance: human +# mechanism: source_reaches_sink + +def do_login(): + form = LoginForm(request.form) + + if not form.validate(): + flash(dumps(form.errors), 'error') + else: + with Session() as session: + user = session.query(User).filter( + User.email == form.email.data).first() + if user is not None and checkpw( + form.password.data.encode('utf-8'), + user.password.encode('utf-8')) and login_user(user): + return redirect("/") + + flash('Invalid Credentials!', 'warning') + logout_user() + + return redirect("/") diff --git a/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-search/vuln.py b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-search/vuln.py new file mode 100644 index 0000000..b115bf0 --- /dev/null +++ b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-account-search/vuln.py @@ -0,0 +1,25 @@ +# Provenance: manuelz120/extremely-vulnerable-flask-app search (vuln). +# repo: manuelz120/extremely-vulnerable-flask-app +# commit: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# parent: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# commit_url: https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L33 +# cve: +# license: GPL-3.0 +# function: search +# relpath: routes/account.py +# provenance: human +# mechanism: source_reaches_sink + +def search(): + search_param = request.args.get('search', '') + with Session() as session: + session.query(Note) + + personal_notes = session.query(Note).filter( + Note.user_id == current_user.id, + text(f"text like '%{search_param}%'")).all() + return render_template( + 'search.html', + search=search_param, + personal_notes=personal_notes, + ) diff --git a/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-signup-validate-token/fixed.py b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-signup-validate-token/fixed.py new file mode 100644 index 0000000..c9d5484 --- /dev/null +++ b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-signup-validate-token/fixed.py @@ -0,0 +1,48 @@ +# Provenance: manuelz120/extremely-vulnerable-flask-app validate_token (fixed). +# repo: manuelz120/extremely-vulnerable-flask-app +# commit: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# parent: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# commit_url: https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/signup.py#L15 +# cve: +# license: GPL-3.0 +# function: validate_token +# relpath: routes/signup.py +# provenance: human +# mechanism: source_reaches_sink + +def do_signup(): + form = RegistrationForm(request.form) + + if not form.validate(): + flash(dumps(form.errors), 'error') + else: + with Session() as session: + user_already_exists = session.query( + session.query(User).where( + User.email == form.email.data).exists()).scalar() + + code = form.registration_code.data + token_id = validate_token(code, session) + if token_id is None: + flash("Invalid registration code", 'warning') + return redirect("/signup") + + token = session.get(RegistrationCode, token_id) + if token.code != code: + flash("Unexpected registration code mismatch", 'error') + return redirect("/signup") + + session.delete(token) + + if user_already_exists: + flash("User already exists", 'warning') + return redirect("/signup") + + user = User( + form.email.data, + hashpw(form.password.data.encode('utf-8'), gensalt()).decode()) + + session.add(user) + session.commit() + + return redirect('/home') diff --git a/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-signup-validate-token/vuln.py b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-signup-validate-token/vuln.py new file mode 100644 index 0000000..3158850 --- /dev/null +++ b/benchmarks/pairs/vibe-py/extremely-vulnerable-flask-app-signup-validate-token/vuln.py @@ -0,0 +1,25 @@ +# Provenance: manuelz120/extremely-vulnerable-flask-app validate_token (vuln). +# repo: manuelz120/extremely-vulnerable-flask-app +# commit: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# parent: d5d8875559e21222bbbaadb7b9f0af592c6eb7fa +# commit_url: https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/signup.py#L15 +# cve: +# license: GPL-3.0 +# function: validate_token +# relpath: routes/signup.py +# provenance: human +# mechanism: source_reaches_sink + +def validate_token(code: str, session: Session) -> Union[str, None]: + try: + result = session.execute( + text(f""" + SELECT id, code FROM {RegistrationCode.__tablename__} WHERE code = '{code}' + """)).first() + + if result is None: + return None + + return result.id + except OperationalError: + return None diff --git a/benchmarks/pairs/vibe-py/python-insecure-app-main-try-hack-me/fixed.py b/benchmarks/pairs/vibe-py/python-insecure-app-main-try-hack-me/fixed.py new file mode 100644 index 0000000..61b9f51 --- /dev/null +++ b/benchmarks/pairs/vibe-py/python-insecure-app-main-try-hack-me/fixed.py @@ -0,0 +1,35 @@ +# Provenance: kolega-ai/realvuln-python-insecure-app try_hack_me (fixed). +# repo: kolega-ai/realvuln-python-insecure-app +# commit: 80b894a2f754590747a31c92b2088be086421953 +# parent: 80b894a2f754590747a31c92b2088be086421953 +# commit_url: https://github.com/kolega-ai/realvuln-python-insecure-app/blob/80b894a2f754590747a31c92b2088be086421953/app/main.py#L41 +# cve: +# license: GPL-3.0 +# function: try_hack_me +# relpath: app/main.py +# provenance: human +# mechanism: source_reaches_sink + +async def try_hack_me(name: str = config.SUPER_SECRET_NAME): + """ + Root endpoint that greets the user and provides a random text. + + Args: + name (str, optional): Name of the user. Defaults to SUPER_SECRET_NAME. + + Returns: + str: HTML content with a greeting and a public ip response. + """ + try: + # Get the public IP address from an external service + public_ip_response = requests.get(config.PUBLIC_IP_SERVICE_URL) + public_ip_response.raise_for_status() + except (requests.HTTPError, requests.exceptions.InvalidSchema): + public_ip = "Unknown" + else: + public_ip = public_ip_response.text + name = name or config.SUPER_SECRET_NAME + content = f"

Hello, {name}!

Public IP: {public_ip}

" + # https://fastapi.tiangolo.com/advanced/custom-response/#return-a-response + # FIXME: return HTMLResponse(content) + return Template(content).render() diff --git a/benchmarks/pairs/vibe-py/python-insecure-app-main-try-hack-me/vuln.py b/benchmarks/pairs/vibe-py/python-insecure-app-main-try-hack-me/vuln.py new file mode 100644 index 0000000..686b812 --- /dev/null +++ b/benchmarks/pairs/vibe-py/python-insecure-app-main-try-hack-me/vuln.py @@ -0,0 +1,35 @@ +# Provenance: kolega-ai/realvuln-python-insecure-app try_hack_me (vuln). +# repo: kolega-ai/realvuln-python-insecure-app +# commit: 80b894a2f754590747a31c92b2088be086421953 +# parent: 80b894a2f754590747a31c92b2088be086421953 +# commit_url: https://github.com/kolega-ai/realvuln-python-insecure-app/blob/80b894a2f754590747a31c92b2088be086421953/app/main.py#L41 +# cve: +# license: GPL-3.0 +# function: try_hack_me +# relpath: app/main.py +# provenance: human +# mechanism: source_reaches_sink + +async def try_hack_me(name: str = config.SUPER_SECRET_NAME): + """ + Root endpoint that greets the user and provides a random text. + + Args: + name (str, optional): Name of the user. Defaults to SUPER_SECRET_NAME. + + Returns: + str: HTML content with a greeting and a public ip response. + """ + try: + # Get the public IP address from an external service + public_ip_response = requests.get(config.PUBLIC_IP_SERVICE_URL) + public_ip_response.raise_for_status() + except (requests.HTTPError, requests.exceptions.InvalidSchema): + public_ip = "Unknown" + else: + public_ip = public_ip_response.text + name = name or config.SUPER_SECRET_NAME + content = f"

Hello, {name}!

Public IP: {public_ip}

" + # https://fastapi.tiangolo.com/advanced/custom-response/#return-a-response + # FIXME: return HTMLResponse(content) + return Template(content).render() diff --git a/benchmarks/pairs/vibe-py/pythonssti-main-read-root/fixed.py b/benchmarks/pairs/vibe-py/pythonssti-main-read-root/fixed.py new file mode 100644 index 0000000..bebe274 --- /dev/null +++ b/benchmarks/pairs/vibe-py/pythonssti-main-read-root/fixed.py @@ -0,0 +1,24 @@ +# Provenance: TheWation/PythonSSTI read_root (fixed). +# repo: TheWation/PythonSSTI +# commit: 76339c27d5a870c0a81bad6aee5f9bd516592145 +# parent: 76339c27d5a870c0a81bad6aee5f9bd516592145 +# commit_url: https://github.com/TheWation/PythonSSTI/blob/76339c27d5a870c0a81bad6aee5f9bd516592145/main.py#L14 +# cve: +# license: MIT +# function: read_root +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink + +async def read_root(username=None): + + username = username or "Guest" + Jinja2 = Environment() + + # Vulnerable Implementation + output = Jinja2.from_string("Welcome " + username + "!").render() + + # Safe Implementation + # output = Jinja2.from_string("Welcome {{ user_name }}!").render(user_name=username) + + return HTMLResponse(content=output) diff --git a/benchmarks/pairs/vibe-py/pythonssti-main-read-root/vuln.py b/benchmarks/pairs/vibe-py/pythonssti-main-read-root/vuln.py new file mode 100644 index 0000000..baafaee --- /dev/null +++ b/benchmarks/pairs/vibe-py/pythonssti-main-read-root/vuln.py @@ -0,0 +1,24 @@ +# Provenance: TheWation/PythonSSTI read_root (vuln). +# repo: TheWation/PythonSSTI +# commit: 76339c27d5a870c0a81bad6aee5f9bd516592145 +# parent: 76339c27d5a870c0a81bad6aee5f9bd516592145 +# commit_url: https://github.com/TheWation/PythonSSTI/blob/76339c27d5a870c0a81bad6aee5f9bd516592145/main.py#L14 +# cve: +# license: MIT +# function: read_root +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink + +async def read_root(username=None): + + username = username or "Guest" + Jinja2 = Environment() + + # Vulnerable Implementation + output = Jinja2.from_string("Welcome " + username + "!").render() + + # Safe Implementation + # output = Jinja2.from_string("Welcome {{ user_name }}!").render(user_name=username) + + return HTMLResponse(content=output) diff --git a/benchmarks/pairs/vibe-py/recipes.toml b/benchmarks/pairs/vibe-py/recipes.toml new file mode 100644 index 0000000..53dc4e5 --- /dev/null +++ b/benchmarks/pairs/vibe-py/recipes.toml @@ -0,0 +1,1119 @@ +# vibe-py harvest recipes generated from Real-Vuln-Benchmark ground truth by build_recipes.py. +# Vulnerable function = vuln side; a false-positive trap from the same repo = fixed twin. +# Review each row before trusting it as a label; regenerate with build_recipes.py. + +[[recipe]] +name = "damn-vulnerable-graphql-application-views-mutate" +host = "github" +repo = "dolevf/Damn-Vulnerable-GraphQL-Application" +parent = "a961308c02d1fb462b192681c336b0739e432da7" +commit = "a961308c02d1fb462b192681c336b0739e432da7" +path = "core/views.py" +fix_path = "core/views.py" +mode = "enclosing" +line = 211 +fix_line = 234 +function = "mutate" +language = "python" +relpath = "core/views.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-78" +class = "command_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-damn-vulnerable-graphql-application" +framework = "flask" +gt_id = "dvga-002" +trap_id = "dvga-fp-001" +acceptable_cwes = ["CWE-78", "CWE-77", "CWE-88"] +evidence = "OS command injection in ImportPaste mutation. User-controlled host, port, path, and scheme parameters are used to construct a URL that is passed to os.popen via helpers.run_cmd(f'curl --insecure {url}'). In easy mode, strip_dangerous_charac" +commit_url = "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L211" +split = "train" + +[[recipe]] +name = "damn-vulnerable-graphql-application-views-resolve-pastes" +host = "github" +repo = "dolevf/Damn-Vulnerable-GraphQL-Application" +parent = "a961308c02d1fb462b192681c336b0739e432da7" +commit = "a961308c02d1fb462b192681c336b0739e432da7" +path = "core/views.py" +fix_path = "core/views.py" +mode = "enclosing" +line = 320 +fix_line = 234 +function = "resolve_pastes" +language = "python" +relpath = "core/views.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-damn-vulnerable-graphql-application" +framework = "flask" +gt_id = "dvga-001" +trap_id = "dvga-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via unsanitized 'filter' GraphQL argument directly interpolated into raw SQL using Python string formatting: result.filter(text(\"title = '%s' or content = '%s'\" % (filter, filter))). An attacker can inject arbitrary SQL via th" +commit_url = "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L320" +split = "holdout" + +[[recipe]] +name = "damn-vulnerable-graphql-application-views-resolve-system-dia" +host = "github" +repo = "dolevf/Damn-Vulnerable-GraphQL-Application" +parent = "a961308c02d1fb462b192681c336b0739e432da7" +commit = "a961308c02d1fb462b192681c336b0739e432da7" +path = "core/views.py" +fix_path = "core/views.py" +mode = "enclosing" +line = 345 +fix_line = 234 +function = "resolve_system_diagnostics" +language = "python" +relpath = "core/views.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-78" +class = "command_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-damn-vulnerable-graphql-application" +framework = "flask" +gt_id = "dvga-004" +trap_id = "dvga-fp-001" +acceptable_cwes = ["CWE-78", "CWE-77", "CWE-88"] +evidence = "OS command injection via the 'cmd' argument in the systemDiagnostics GraphQL query. In easy mode, security.allowed_cmds() always returns True, so user-supplied cmd is passed directly to os.popen via helpers.run_cmd(cmd). Requires valid admi" +commit_url = "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L345" +split = "train" + +[[recipe]] +name = "djangoat-analytics-hits-by-ip" +host = "github" +repo = "Contrast-Security-OSS/DjanGoat" +parent = "082c88bfefced508ae083b92b459c808f95e905e" +commit = "082c88bfefced508ae083b92b459c808f95e905e" +path = "app/models/Analytics/analytics.py" +fix_path = "app/models/Analytics/analytics.py" +mode = "enclosing" +line = 57 +fix_line = 72 +function = "hits_by_ip" +language = "python" +relpath = "app/models/Analytics/analytics.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-djangoat" +framework = "django" +gt_id = "djangoat-002" +trap_id = "djangoat-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via string formatting in raw query. Both ip and col parameters are interpolated directly into SQL: 'SELECT %s FROM %s WHERE ip_address=\\'%s\\' ORDER BY id DESC' % (col, table_name, ip). The ip parameter comes from request.GET['" +commit_url = "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/models/Analytics/analytics.py#L57" +split = "holdout" + +[[recipe]] +name = "djangoat-views-reset-password" +host = "github" +repo = "Contrast-Security-OSS/DjanGoat" +parent = "082c88bfefced508ae083b92b459c808f95e905e" +commit = "082c88bfefced508ae083b92b459c808f95e905e" +path = "app/views/password_resets/views.py" +fix_path = "app/views/password_resets/views.py" +mode = "enclosing" +line = 84 +fix_line = 17 +function = "reset_password" +language = "python" +relpath = "app/views/password_resets/views.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-502" +class = "insecure_deserialization" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-djangoat" +framework = "django" +gt_id = "djangoat-003" +trap_id = "djangoat-fp-003" +acceptable_cwes = ["CWE-502", "CWE-94", "CWE-306"] +evidence = "Remote code execution via insecure deserialization. User-controlled POST parameter 'user' is base64-decoded and passed to pickle.loads(): user = pickle.loads(base64.b64decode(encoded_user)). An attacker can craft a malicious pickle payload " +commit_url = "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/password_resets/views.py#L84" +split = "train" + +[[recipe]] +name = "djangoat-views-user-view" +host = "github" +repo = "Contrast-Security-OSS/DjanGoat" +parent = "082c88bfefced508ae083b92b459c808f95e905e" +commit = "082c88bfefced508ae083b92b459c808f95e905e" +path = "app/views/users/views.py" +fix_path = "app/models/Analytics/analytics.py" +mode = "enclosing" +line = 74 +fix_line = 72 +function = "user_view" +language = "python" +relpath = "app/views/users/views.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-djangoat" +framework = "django" +gt_id = "djangoat-001" +trap_id = "djangoat-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via string formatting in raw query. user_id_form from POST data is interpolated directly into SQL: User.objects.raw('SELECT * FROM %s WHERE user_id=\\'%s\\' ORDER BY is_admin=\\'0\\'' % (table_name, user_id_form)). An attacker can" +commit_url = "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/users/views.py#L74" +split = "holdout" + +[[recipe]] +name = "dsvpwa-attacks-run" +host = "github" +repo = "sgabe/DSVPWA" +parent = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +commit = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +path = "dsvpwa/attacks.py" +fix_path = "dsvpwa/attacks.py" +mode = "enclosing" +line = 39 +fix_line = 88 +function = "run" +language = "python" +relpath = "dsvpwa/attacks.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-dsvpwa" +framework = "None" +gt_id = "dsvpwa-001" +trap_id = "dsvpwa-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via string concatenation in SQLinjection.run(). The 'id' parameter from request is concatenated directly into SQL: cursor.execute(\"SELECT ... WHERE id=\" + id). No parameterization or input validation." +commit_url = "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/attacks.py#L39" +split = "train" + +[[recipe]] +name = "dsvpwa-dsvpwa-main" +host = "github" +repo = "sgabe/DSVPWA" +parent = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +commit = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +path = "dsvpwa.py" +fix_path = "dsvpwa/attacks.py" +mode = "enclosing" +line = 37 +fix_line = 88 +function = "main" +language = "python" +relpath = "dsvpwa.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-326" +class = "security_misconfiguration" +mechanism = "permissive_default" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-dsvpwa" +framework = "None" +gt_id = "dsvpwa-020" +trap_id = "dsvpwa-fp-001" +acceptable_cwes = ["CWE-326", "CWE-327", "CWE-757"] +evidence = "Insecure SSL/TLS configuration in main(). Three dangerous SSL options are explicitly enabled: (1) SSLv3 re-enabled via ctx.options &= ~ssl.OP_NO_SSLv3 (vulnerable to POODLE), (2) TLS compression re-enabled via &= ~ssl.OP_NO_COMPRESSION (vul" +commit_url = "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa.py#L37" +split = "holdout" + +[[recipe]] +name = "dsvpwa-handlers-do-bdr" +host = "github" +repo = "sgabe/DSVPWA" +parent = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +commit = "a06abb306ed3699bb2f073e3f41d3d9c57012819" +path = "dsvpwa/handlers.py" +fix_path = "dsvpwa/attacks.py" +mode = "enclosing" +line = 183 +fix_line = 88 +function = "do_BDR" +language = "python" +relpath = "dsvpwa/handlers.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-78" +class = "command_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-dsvpwa" +framework = "None" +gt_id = "dsvpwa-014" +trap_id = "dsvpwa-fp-001" +acceptable_cwes = ["CWE-78", "CWE-77"] +evidence = "Critical command injection via custom HTTP method BDR in do_BDR(). The URL path (self.path[1:]) is passed directly to subprocess.check_output() with shell=True: subprocess.check_output(self.path[1:], shell=True, ...). Any command can be exe" +commit_url = "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/handlers.py#L183" +split = "train" + +[[recipe]] +name = "dsvw-dsvw-do-get" +host = "github" +repo = "stamparm/DSVW" +parent = "7d40f4b7939c901610ed9b85724552d60e7d63fa" +commit = "7d40f4b7939c901610ed9b85724552d60e7d63fa" +path = "dsvw.py" +fix_path = "dsvw.py" +mode = "enclosing" +line = 30 +fix_line = 67 +function = "do_GET" +language = "python" +relpath = "dsvw.py" +license = "Unlicense" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-dsvw" +framework = "None" +gt_id = "dsvw-001" +trap_id = "dsvw-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via id parameter directly concatenated into SELECT query without sanitization" +commit_url = "https://github.com/stamparm/DSVW/blob/7d40f4b7939c901610ed9b85724552d60e7d63fa/dsvw.py#L30" +split = "holdout" + +[[recipe]] +name = "dvpwa-app-init" +host = "github" +repo = "anxolerd/dvpwa" +parent = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +commit = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +path = "sqli/app.py" +fix_path = "sqli/dao/user.py" +mode = "enclosing" +line = 35 +fix_line = 33 +function = "init" +language = "python" +relpath = "sqli/app.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-16" +class = "security_misconfiguration" +mechanism = "permissive_default" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-dvpwa" +framework = "aiohttp" +gt_id = "dvpwa-007" +trap_id = "dvpwa-fp-001" +acceptable_cwes = ["CWE-16", "CWE-79"] +evidence = "Jinja2 template engine configured with autoescape=False. This disables automatic HTML escaping for all template variables, making every unescaped template expression a potential XSS sink. This is the root cause of all stored XSS vulnerabili" +commit_url = "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/app.py#L35" +split = "train" + +[[recipe]] +name = "dvpwa-middlewares-session-middleware" +host = "github" +repo = "anxolerd/dvpwa" +parent = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +commit = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +path = "sqli/middlewares.py" +fix_path = "sqli/dao/user.py" +mode = "enclosing" +line = 20 +fix_line = 33 +function = "session_middleware" +language = "python" +relpath = "sqli/middlewares.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-1004" +class = "security_misconfiguration" +mechanism = "permissive_default" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-dvpwa" +framework = "aiohttp" +gt_id = "dvpwa-010" +trap_id = "dvpwa-fp-001" +acceptable_cwes = ["CWE-1004", "CWE-614", "CWE-16"] +evidence = "Session cookie created with httponly=False. The RedisStorage is configured with httponly=False, meaning the AIOHTTP_SESSION cookie is accessible to JavaScript via document.cookie. Combined with the stored XSS vulnerabilities, this enables s" +commit_url = "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/middlewares.py#L20" +split = "holdout" + +[[recipe]] +name = "dvpwa-student-create" +host = "github" +repo = "anxolerd/dvpwa" +parent = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +commit = "a1d8f89fac2e57093189853c6527c2b01fc1d9c1" +path = "sqli/dao/student.py" +fix_path = "sqli/dao/user.py" +mode = "enclosing" +line = 42 +fix_line = 33 +function = "create" +language = "python" +relpath = "sqli/dao/student.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-dvpwa" +framework = "aiohttp" +gt_id = "dvpwa-001" +trap_id = "dvpwa-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via Python string formatting. The 'name' parameter from user input is interpolated directly into the SQL query using Python's % operator: \"VALUES ('%(name)s')\" % {'name': name}. The resulting string is then passed to cur.execu" +commit_url = "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/dao/student.py#L42" +split = "train" + +[[recipe]] +name = "extremely-vulnerable-flask-app-account-before-request" +host = "github" +repo = "manuelz120/extremely-vulnerable-flask-app" +parent = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +commit = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +path = "routes/account.py" +fix_path = "routes/login.py" +mode = "enclosing" +line = 118 +fix_line = 30 +function = "before_request" +language = "python" +relpath = "routes/account.py" +license = "GPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-502" +class = "insecure_deserialization" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-extremely-vulnerable-flask-app" +framework = "flask" +gt_id = "extremely-vulnerable-flask-app-004" +trap_id = "extremely-vulnerable-flask-app-fp-001" +acceptable_cwes = ["CWE-502", "CWE-94"] +evidence = "Remote code execution via insecure deserialization. The 'preferences' cookie is base64-decoded and passed directly to pickle.loads() (imported as 'loads' from pickle). An attacker can craft a malicious pickle payload, base64-encode it, and " +commit_url = "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L118" +split = "holdout" + +[[recipe]] +name = "extremely-vulnerable-flask-app-account-search" +host = "github" +repo = "manuelz120/extremely-vulnerable-flask-app" +parent = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +commit = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +path = "routes/account.py" +fix_path = "routes/login.py" +mode = "enclosing" +line = 33 +fix_line = 30 +function = "search" +language = "python" +relpath = "routes/account.py" +license = "GPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-extremely-vulnerable-flask-app" +framework = "flask" +gt_id = "extremely-vulnerable-flask-app-002" +trap_id = "extremely-vulnerable-flask-app-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via f-string interpolation of user-supplied search parameter into a raw SQL text() fragment: text(f\"text like '%{search_param}%'\"). The search_param comes directly from request.args.get('search') with no sanitization." +commit_url = "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L33" +split = "train" + +[[recipe]] +name = "extremely-vulnerable-flask-app-signup-validate-token" +host = "github" +repo = "manuelz120/extremely-vulnerable-flask-app" +parent = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +commit = "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa" +path = "routes/signup.py" +fix_path = "routes/signup.py" +mode = "enclosing" +line = 15 +fix_line = 41 +function = "validate_token" +language = "python" +relpath = "routes/signup.py" +license = "GPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-extremely-vulnerable-flask-app" +framework = "flask" +gt_id = "extremely-vulnerable-flask-app-001" +trap_id = "extremely-vulnerable-flask-app-fp-002" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via f-string interpolation of user-supplied registration code directly into a raw SQL query: text(f\"SELECT id, code FROM ... WHERE code = '{code}'\"). An attacker can inject arbitrary SQL through the registration_code form fiel" +commit_url = "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/signup.py#L15" +split = "holdout" + +[[recipe]] +name = "python-insecure-app-main-try-hack-me" +host = "github" +repo = "kolega-ai/realvuln-python-insecure-app" +parent = "80b894a2f754590747a31c92b2088be086421953" +commit = "80b894a2f754590747a31c92b2088be086421953" +path = "app/main.py" +fix_path = "app/main.py" +mode = "enclosing" +line = 41 +fix_line = 19 +function = "try_hack_me" +language = "python" +relpath = "app/main.py" +license = "GPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-1336" +class = "ssti" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-python-insecure-app" +framework = "fastapi" +gt_id = "python-insecure-app-001" +trap_id = "python-insecure-app-fp-001" +acceptable_cwes = ["CWE-1336", "CWE-94", "CWE-95"] +evidence = "Server-Side Template Injection via Jinja2. User-controlled 'name' query parameter is interpolated into an f-string on line 38 to build 'content', which is then passed to Template(content).render() on line 41. An attacker can inject Jinja2 t" +commit_url = "https://github.com/kolega-ai/realvuln-python-insecure-app/blob/80b894a2f754590747a31c92b2088be086421953/app/main.py#L41" +split = "train" + +[[recipe]] +name = "pythonssti-main-read-root" +host = "github" +repo = "TheWation/PythonSSTI" +parent = "76339c27d5a870c0a81bad6aee5f9bd516592145" +commit = "76339c27d5a870c0a81bad6aee5f9bd516592145" +path = "main.py" +fix_path = "main.py" +mode = "enclosing" +line = 14 +fix_line = 17 +function = "read_root" +language = "python" +relpath = "main.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-79" +class = "reflected_xss" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-pythonssti" +framework = "fastapi" +gt_id = "python-ssti-002" +trap_id = "python-ssti-fp-001" +acceptable_cwes = ["CWE-79", "CWE-80"] +evidence = "The Jinja2 Environment is created with default settings (autoescape=False). User input from the 'username' query parameter is concatenated into the template string and the rendered output is returned as HTMLResponse. An attacker can inject " +commit_url = "https://github.com/TheWation/PythonSSTI/blob/76339c27d5a870c0a81bad6aee5f9bd516592145/main.py#L14" +split = "holdout" + +[[recipe]] +name = "threatbyte-api-v1-delete" +host = "github" +repo = "anotherik/ThreatByte" +parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +path = "server/api/v1/api_v1.py" +fix_path = "server/api/v1/api_v1.py" +mode = "enclosing" +line = 223 +fix_line = 123 +function = "delete" +language = "python" +relpath = "server/api/v1/api_v1.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-862" +class = "broken_access_control" +mechanism = "missing_auth_guard" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-threatbyte" +framework = "flask" +gt_id = "threatbyte-011" +trap_id = "threatbyte-fp-004" +acceptable_cwes = ["CWE-862", "CWE-306", "CWE-284", "CWE-287"] +evidence = "BFLA (Broken Function Level Authorization) on the delete-user API endpoint. The @token_required decorator is commented out (lines 221-222), so DELETE /api/v1/delete-user/ can be called by anyone without authentication to delete any" +commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L223" +split = "train" + +[[recipe]] +name = "threatbyte-api-v1-get" +host = "github" +repo = "anotherik/ThreatByte" +parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +path = "server/api/v1/api_v1.py" +fix_path = "server/api/v1/api_v1.py" +mode = "enclosing" +line = 154 +fix_line = 123 +function = "get" +language = "python" +relpath = "server/api/v1/api_v1.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "broken_access_control" +mechanism = "missing_auth_guard" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-threatbyte" +framework = "flask" +gt_id = "threatbyte-010" +trap_id = "threatbyte-fp-004" +acceptable_cwes = ["CWE-639", "CWE-284", "CWE-862", "CWE-863"] +evidence = "BOLA (Broken Object Level Authorization) on the API profile endpoint. The @token_required decorator is commented out (line 152-153), so /api/v1/profile?user_id=X returns any user's profile (username, email, country, role, permissions, team)" +commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L154" +split = "holdout" + +[[recipe]] +name = "threatbyte-routes-dashboard" +host = "github" +repo = "anotherik/ThreatByte" +parent = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +commit = "c4db46f72586c4ecb65859051ea037018dbf0e5e" +path = "server/routes.py" +fix_path = "server/routes.py" +mode = "enclosing" +line = 137 +fix_line = 45 +function = "dashboard" +language = "python" +relpath = "server/routes.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-threatbyte" +framework = "flask" +gt_id = "threatbyte-001" +trap_id = "threatbyte-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943", "CWE-306"] +evidence = "SQL injection via f-string interpolation of user-controlled search_query parameter directly into SQL: s_query = f\"SELECT filename FROM files WHERE user_id = {user_id} AND filename LIKE '%{search_query}%'\". The search_query comes from reques" +commit_url = "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/routes.py#L137" +split = "train" + +[[recipe]] +name = "vampi-books-get-by-title" +host = "github" +repo = "erev0s/VAmPI" +parent = "1713b54b601ad29582581eeda4b31fceb1319874" +commit = "1713b54b601ad29582581eeda4b31fceb1319874" +path = "api_views/books.py" +fix_path = "api_views/books.py" +mode = "enclosing" +line = 51 +fix_line = 62 +function = "get_by_title" +language = "python" +relpath = "api_views/books.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "broken_access_control" +mechanism = "missing_auth_guard" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vampi" +framework = "flask" +gt_id = "vampi-003" +trap_id = "vampi-fp-003" +acceptable_cwes = ["CWE-639", "CWE-284", "CWE-285", "CWE-862"] +evidence = "Broken Object Level Authorization (BOLA). When vuln=1, get_by_title queries the book by title only (Book.query.filter_by(book_title=str(book_title)).first()) without verifying the authenticated user is the owner. Any authenticated user can " +commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51" +split = "holdout" + +[[recipe]] +name = "vampi-user-model-get-user" +host = "github" +repo = "erev0s/VAmPI" +parent = "1713b54b601ad29582581eeda4b31fceb1319874" +commit = "1713b54b601ad29582581eeda4b31fceb1319874" +path = "models/user_model.py" +fix_path = "api_views/users.py" +mode = "enclosing" +line = 72 +fix_line = 55 +function = "get_user" +language = "python" +relpath = "models/user_model.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vampi" +framework = "flask" +gt_id = "vampi-001" +trap_id = "vampi-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via f-string interpolation of unsanitized username path parameter into raw SQL query: user_query = f\"SELECT * FROM users WHERE username = '{username}'\" followed by db.session.execute(text(user_query)). Reachable from GET /user" +commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/models/user_model.py#L72" +split = "train" + +[[recipe]] +name = "vampi-users-update-password" +host = "github" +repo = "erev0s/VAmPI" +parent = "1713b54b601ad29582581eeda4b31fceb1319874" +commit = "1713b54b601ad29582581eeda4b31fceb1319874" +path = "api_views/users.py" +fix_path = "api_views/users.py" +mode = "enclosing" +line = 187 +fix_line = 55 +function = "update_password" +language = "python" +relpath = "api_views/users.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-639" +class = "broken_access_control" +mechanism = "missing_auth_guard" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vampi" +framework = "flask" +gt_id = "vampi-002" +trap_id = "vampi-fp-001" +acceptable_cwes = ["CWE-639", "CWE-284", "CWE-285", "CWE-862"] +evidence = "Unauthorized password change (IDOR). When vuln=1, the update_password function uses the username from the URL path parameter to look up the user whose password to change, instead of using the authenticated user from the JWT token (resp['sub" +commit_url = "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187" +split = "holdout" + +[[recipe]] +name = "vfapi-main-delete-user" +host = "github" +repo = "naryal2580/vfapi" +parent = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +commit = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +path = "main.py" +fix_path = "main.py" +mode = "enclosing" +line = 231 +fix_line = 220 +function = "delete_user" +language = "python" +relpath = "main.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vfapi" +framework = "fastapi" +gt_id = "vfapi-003" +trap_id = "vfapi-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via f-string interpolation of username query parameter into DELETE SQL query: f'DELETE FROM users WHERE username = \"{username}\";'. No sanitization or parameterization." +commit_url = "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L231" +split = "train" + +[[recipe]] +name = "vfapi-main-put-user" +host = "github" +repo = "naryal2580/vfapi" +parent = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +commit = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +path = "main.py" +fix_path = "main.py" +mode = "enclosing" +line = 188 +fix_line = 220 +function = "put_user" +language = "python" +relpath = "main.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vfapi" +framework = "fastapi" +gt_id = "vfapi-002" +trap_id = "vfapi-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via f-string interpolation of all user-supplied fields (name, username, password, address, email, contact) into an INSERT SQL query. All fields from the User model body are directly embedded in the query string without paramet" +commit_url = "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L188" +split = "holdout" + +[[recipe]] +name = "vfapi-main-sql-return-users-from-username" +host = "github" +repo = "naryal2580/vfapi" +parent = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +commit = "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3" +path = "main.py" +fix_path = "main.py" +mode = "enclosing" +line = 182 +fix_line = 220 +function = "sql_return_users_from_username" +language = "python" +relpath = "main.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vfapi" +framework = "fastapi" +gt_id = "vfapi-001" +trap_id = "vfapi-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via f-string interpolation of username query parameter directly into SQL query: f'SELECT * FROM users WHERE username = \"{username}\";'. No sanitization or parameterization." +commit_url = "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L182" +split = "train" + +[[recipe]] +name = "vulnerable-python-apps-bad-mvc-do-get" +host = "github" +repo = "LinuxUser255/Vulnerable_Python_Apps" +parent = "6336c77e6db523a5df34c4b628dff4159110797a" +commit = "6336c77e6db523a5df34c4b628dff4159110797a" +path = "Insecure_Frameworks/bad_mvc.py" +fix_path = "SQLI/main.py" +mode = "enclosing" +line = 37 +fix_line = 44 +function = "do_GET" +language = "python" +relpath = "Insecure_Frameworks/bad_mvc.py" +license = "GPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-79" +class = "reflected_xss" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulnerable-python-apps" +framework = "flask" +gt_id = "vulnerable-python-apps-009" +trap_id = "vulnerable-python-apps-fp-001" +acceptable_cwes = ["CWE-79", "CWE-80"] +evidence = "XSS via unescaped output. The username variable (derived from decrypting a user-supplied cookie at line 26) is rendered into HTML without any escaping: bytes('Hello %s' % username, 'utf-8'). An attacker who controls the encrypted cookie val" +commit_url = "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Insecure_Frameworks/bad_mvc.py#L37" +split = "holdout" + +[[recipe]] +name = "vulnerable-python-apps-dir-traversal-do-get" +host = "github" +repo = "LinuxUser255/Vulnerable_Python_Apps" +parent = "6336c77e6db523a5df34c4b628dff4159110797a" +commit = "6336c77e6db523a5df34c4b628dff4159110797a" +path = "Directory_Traversal/Dir_Traversal.py" +fix_path = "SQLI/main.py" +mode = "enclosing" +line = 22 +fix_line = 44 +function = "do_GET" +language = "python" +relpath = "Directory_Traversal/Dir_Traversal.py" +license = "GPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-22" +class = "path_traversal" +mechanism = "path_join_user_input" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulnerable-python-apps" +framework = "flask" +gt_id = "vulnerable-python-apps-001" +trap_id = "vulnerable-python-apps-fp-001" +acceptable_cwes = ["CWE-22", "CWE-23", "CWE-36", "CWE-73"] +evidence = "Arbitrary file read via path traversal. self.path from the HTTP request is concatenated to cwd at line 14 and opened at line 22 via open(path).read(). The regex filter on line 10-11 (r'/\\.\\.\\/\\.\\./' with re.match) is trivially bypassable — " +commit_url = "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Directory_Traversal/Dir_Traversal.py#L22" +split = "train" + +[[recipe]] +name = "vulnerable-python-apps-main-get-db-connection" +host = "github" +repo = "LinuxUser255/Vulnerable_Python_Apps" +parent = "6336c77e6db523a5df34c4b628dff4159110797a" +commit = "6336c77e6db523a5df34c4b628dff4159110797a" +path = "SQLI/main.py" +fix_path = "SQLI/main.py" +mode = "enclosing" +line = 70 +fix_line = 44 +function = "get_db_connection" +language = "python" +relpath = "SQLI/main.py" +license = "GPL-3.0" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulnerable-python-apps" +framework = "flask" +gt_id = "vulnerable-python-apps-002" +trap_id = "vulnerable-python-apps-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via string concatenation: conn.execute('SELECT * FROM posts WHERE id = '+post_id). The post_id parameter is concatenated directly into the SQL query. The regex filter on line 67-68 (r'^\\d+$' with re.M) is weak because the re.M" +commit_url = "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/SQLI/main.py#L70" +split = "holdout" + +[[recipe]] +name = "vulnpy-cmdi-do-os-system" +host = "github" +repo = "Contrast-Security-OSS/vulnpy" +parent = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +commit = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +path = "src/vulnpy/trigger/cmdi.py" +fix_path = "src/vulnpy/trigger/sqli.py" +mode = "enclosing" +line = 6 +fix_line = 25 +function = "do_os_system" +language = "python" +relpath = "src/vulnpy/trigger/cmdi.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-78" +class = "command_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulnpy" +framework = "None" +gt_id = "vulnpy-001" +trap_id = "vulnpy-fp-001" +acceptable_cwes = ["CWE-78", "CWE-77"] +evidence = "User-controlled command passed directly to os.system(command) with no sanitization, enabling arbitrary OS command execution" +commit_url = "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L6" +split = "train" + +[[recipe]] +name = "vulnpy-cmdi-do-subprocess-popen" +host = "github" +repo = "Contrast-Security-OSS/vulnpy" +parent = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +commit = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +path = "src/vulnpy/trigger/cmdi.py" +fix_path = "src/vulnpy/trigger/sqli.py" +mode = "enclosing" +line = 10 +fix_line = 25 +function = "do_subprocess_popen" +language = "python" +relpath = "src/vulnpy/trigger/cmdi.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-78" +class = "command_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulnpy" +framework = "None" +gt_id = "vulnpy-002" +trap_id = "vulnpy-fp-001" +acceptable_cwes = ["CWE-78", "CWE-77"] +evidence = "User-controlled command passed to subprocess.Popen with shell=True, enabling arbitrary OS command execution" +commit_url = "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L10" +split = "holdout" + +[[recipe]] +name = "vulnpy-deserialization-do-pickle-load" +host = "github" +repo = "Contrast-Security-OSS/vulnpy" +parent = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +commit = "d1d0f64c06f038fe59a049e0e6001bfaf9839697" +path = "src/vulnpy/trigger/deserialization.py" +fix_path = "src/vulnpy/trigger/deserialization.py" +mode = "enclosing" +line = 8 +fix_line = 7 +function = "do_pickle_load" +language = "python" +relpath = "src/vulnpy/trigger/deserialization.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-502" +class = "insecure_deserialization" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulnpy" +framework = "None" +gt_id = "vulnpy-003" +trap_id = "vulnpy-fp-011" +acceptable_cwes = ["CWE-502", "CWE-94"] +evidence = "User input encoded to bytes and passed to pickle.load(), which can execute arbitrary code via crafted pickle payloads" +commit_url = "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/deserialization.py#L8" +split = "train" + +[[recipe]] +name = "vulpy-libuser-create" +host = "github" +repo = "fportantier/vulpy" +parent = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +commit = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +path = "bad/libuser.py" +fix_path = "bad/libposts.py" +mode = "enclosing" +line = 25 +fix_line = 14 +function = "create" +language = "python" +relpath = "bad/libuser.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulpy" +framework = "flask" +gt_id = "vulpy-002" +trap_id = "vulpy-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via %-formatting in INSERT statement: c.execute(\"INSERT INTO users ... VALUES ('%s', '%s', ...\" %(username, password, ...)). User-supplied username and password are interpolated via % string formatting into the SQL query." +commit_url = "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L25" +split = "holdout" + +[[recipe]] +name = "vulpy-libuser-login" +host = "github" +repo = "fportantier/vulpy" +parent = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +commit = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +path = "bad/libuser.py" +fix_path = "bad/libposts.py" +mode = "enclosing" +line = 12 +fix_line = 14 +function = "login" +language = "python" +relpath = "bad/libuser.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulpy" +framework = "flask" +gt_id = "vulpy-001" +trap_id = "vulpy-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via string format in login query: c.execute(\"SELECT * FROM users WHERE username = '{}' and password = '{}'\".format(username, password)). User-supplied username and password from request.form are directly interpolated into the " +commit_url = "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L12" +split = "train" + +[[recipe]] +name = "vulpy-libuser-password-change" +host = "github" +repo = "fportantier/vulpy" +parent = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +commit = "5249cc8b05a1c37f6b2f757b1cf16a509c327122" +path = "bad/libuser.py" +fix_path = "bad/libposts.py" +mode = "enclosing" +line = 53 +fix_line = 14 +function = "password_change" +language = "python" +relpath = "bad/libuser.py" +license = "MIT" +cve = "" +year = 2026 +cwe = "CWE-89" +class = "sql_injection" +mechanism = "source_reaches_sink" +provenance = "human" +origin = "real-vuln-benchmark" +project = "realvuln-vulpy" +framework = "flask" +gt_id = "vulpy-003" +trap_id = "vulpy-fp-001" +acceptable_cwes = ["CWE-89", "CWE-564", "CWE-943"] +evidence = "SQL injection via string format in UPDATE statement: c.execute(\"UPDATE users SET password = '{}' WHERE username = '{}'\".format(password, username)). The password parameter from user input is directly interpolated into the SQL query." +commit_url = "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L53" +split = "holdout" diff --git a/benchmarks/pairs/vibe-py/threatbyte-api-v1-delete/fixed.py b/benchmarks/pairs/vibe-py/threatbyte-api-v1-delete/fixed.py new file mode 100644 index 0000000..08692ee --- /dev/null +++ b/benchmarks/pairs/vibe-py/threatbyte-api-v1-delete/fixed.py @@ -0,0 +1,47 @@ +# Provenance: anotherik/ThreatByte delete (fixed). +# repo: anotherik/ThreatByte +# commit: c4db46f72586c4ecb65859051ea037018dbf0e5e +# parent: c4db46f72586c4ecb65859051ea037018dbf0e5e +# commit_url: https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L223 +# cve: +# license: MIT +# function: delete +# relpath: server/api/v1/api_v1.py +# provenance: human +# mechanism: missing_auth_guard + + def post(self): + """ + Handles user login. + """ + data = request.get_json() # Get data from request body + username = data.get('username') + password = data.get('password') + + if not username or not password: + return {'error': 'Please provide username and password'}, 400 + + with get_db_connection() as conn: + cursor = conn.cursor() + query = "SELECT * FROM users WHERE username = ? OR email = ?" + cursor.execute(query, (username, username)) + user = cursor.fetchone() + + if user and user['password'] == custom_hash(password): + + # Generate JWT + token = jwt.encode({ + 'username': username, + 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) # Token expires in 24 hours + }, Config.SECRET_KEY, algorithm='HS256') + + + # Update last_login timestamp + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute("UPDATE users SET last_login = ? WHERE username = ?", (datetime.datetime.now(), username)) + conn.commit() + + return {'message': 'Login successful', 'token': token}, 200 + else: + return {'error': 'Invalid username or password'}, 401 diff --git a/benchmarks/pairs/vibe-py/threatbyte-api-v1-delete/vuln.py b/benchmarks/pairs/vibe-py/threatbyte-api-v1-delete/vuln.py new file mode 100644 index 0000000..34c4886 --- /dev/null +++ b/benchmarks/pairs/vibe-py/threatbyte-api-v1-delete/vuln.py @@ -0,0 +1,28 @@ +# Provenance: anotherik/ThreatByte delete (vuln). +# repo: anotherik/ThreatByte +# commit: c4db46f72586c4ecb65859051ea037018dbf0e5e +# parent: c4db46f72586c4ecb65859051ea037018dbf0e5e +# commit_url: https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L223 +# cve: +# license: MIT +# function: delete +# relpath: server/api/v1/api_v1.py +# provenance: human +# mechanism: missing_auth_guard + + def delete(self, user_id): + """ + Delete a user from the application. + """ + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if user: + cursor.execute("DELETE FROM users WHERE id = ?", (user_id,)) + conn.commit() + response = make_response(jsonify({'message': 'User deleted successfully'}), 200) + return response + else: + response = make_response(jsonify({'error': 'User not found'}), 404) + return response diff --git a/benchmarks/pairs/vibe-py/threatbyte-api-v1-get/fixed.py b/benchmarks/pairs/vibe-py/threatbyte-api-v1-get/fixed.py new file mode 100644 index 0000000..2c07380 --- /dev/null +++ b/benchmarks/pairs/vibe-py/threatbyte-api-v1-get/fixed.py @@ -0,0 +1,47 @@ +# Provenance: anotherik/ThreatByte get (fixed). +# repo: anotherik/ThreatByte +# commit: c4db46f72586c4ecb65859051ea037018dbf0e5e +# parent: c4db46f72586c4ecb65859051ea037018dbf0e5e +# commit_url: https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L154 +# cve: +# license: MIT +# function: get +# relpath: server/api/v1/api_v1.py +# provenance: human +# mechanism: missing_auth_guard + + def post(self): + """ + Handles user login. + """ + data = request.get_json() # Get data from request body + username = data.get('username') + password = data.get('password') + + if not username or not password: + return {'error': 'Please provide username and password'}, 400 + + with get_db_connection() as conn: + cursor = conn.cursor() + query = "SELECT * FROM users WHERE username = ? OR email = ?" + cursor.execute(query, (username, username)) + user = cursor.fetchone() + + if user and user['password'] == custom_hash(password): + + # Generate JWT + token = jwt.encode({ + 'username': username, + 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) # Token expires in 24 hours + }, Config.SECRET_KEY, algorithm='HS256') + + + # Update last_login timestamp + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute("UPDATE users SET last_login = ? WHERE username = ?", (datetime.datetime.now(), username)) + conn.commit() + + return {'message': 'Login successful', 'token': token}, 200 + else: + return {'error': 'Invalid username or password'}, 401 diff --git a/benchmarks/pairs/vibe-py/threatbyte-api-v1-get/vuln.py b/benchmarks/pairs/vibe-py/threatbyte-api-v1-get/vuln.py new file mode 100644 index 0000000..9e27846 --- /dev/null +++ b/benchmarks/pairs/vibe-py/threatbyte-api-v1-get/vuln.py @@ -0,0 +1,29 @@ +# Provenance: anotherik/ThreatByte get (vuln). +# repo: anotherik/ThreatByte +# commit: c4db46f72586c4ecb65859051ea037018dbf0e5e +# parent: c4db46f72586c4ecb65859051ea037018dbf0e5e +# commit_url: https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L154 +# cve: +# license: MIT +# function: get +# relpath: server/api/v1/api_v1.py +# provenance: human +# mechanism: missing_auth_guard + + def get(self): + """ + Retrieves the profile information of a user based on the provided user ID. + """ + args = profile_query.parse_args() + user_id = args.get('user_id') + + with get_db_connection() as conn: + cursor = conn.cursor() + query = "SELECT username, email, country, role, permissions, team FROM users WHERE id = ?" + cursor.execute(query, (user_id,)) + user_profile = cursor.fetchone() + + if user_profile: + return dict(user_profile), 200 + else: + return {'error': 'User profile not found'}, 404 diff --git a/benchmarks/pairs/vibe-py/threatbyte-routes-dashboard/fixed.py b/benchmarks/pairs/vibe-py/threatbyte-routes-dashboard/fixed.py new file mode 100644 index 0000000..3905798 --- /dev/null +++ b/benchmarks/pairs/vibe-py/threatbyte-routes-dashboard/fixed.py @@ -0,0 +1,53 @@ +# Provenance: anotherik/ThreatByte dashboard (fixed). +# repo: anotherik/ThreatByte +# commit: c4db46f72586c4ecb65859051ea037018dbf0e5e +# parent: c4db46f72586c4ecb65859051ea037018dbf0e5e +# commit_url: https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/routes.py#L137 +# cve: +# license: MIT +# function: dashboard +# relpath: server/routes.py +# provenance: human +# mechanism: source_reaches_sink + +def login(): + """ + Handles user login. + + On GET request: + Renders the login page. + + On POST request: + Retrieves the username and password from the login form. + Queries the database for the user based on the provided username. + Checks if the provided password matches the hashed password stored in the database. + If authentication succeeds, stores the username in the session and redirects to the dashboard. + If authentication fails, renders the login page with an error message. + """ + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + if not username or not password: + return render_template('login.html', error='Please provide username and password') + + # Query the database for the user using parameterized query + with get_db_connection() as conn: + cursor = conn.cursor() + query = "SELECT * FROM users WHERE username = ? OR email = ?" + cursor.execute(query, (username,username)) + user = cursor.fetchone() + + # Check if the user exists and the password matches + if user and user['password'] == custom_hash(password): + session['username'] = user['username'] + session['user_id'] = user['id'] + + # Update last_login timestamp + cursor.execute("UPDATE users SET last_login = ? WHERE username = ?", (datetime.datetime.now(), username)) + conn.commit() + conn.close() + + return redirect(url_for('dashboard')) + else: + return render_template('login.html', error='Invalid username or password') + return render_template('login.html') diff --git a/benchmarks/pairs/vibe-py/threatbyte-routes-dashboard/vuln.py b/benchmarks/pairs/vibe-py/threatbyte-routes-dashboard/vuln.py new file mode 100644 index 0000000..95b6cd2 --- /dev/null +++ b/benchmarks/pairs/vibe-py/threatbyte-routes-dashboard/vuln.py @@ -0,0 +1,36 @@ +# Provenance: anotherik/ThreatByte dashboard (vuln). +# repo: anotherik/ThreatByte +# commit: c4db46f72586c4ecb65859051ea037018dbf0e5e +# parent: c4db46f72586c4ecb65859051ea037018dbf0e5e +# commit_url: https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/routes.py#L137 +# cve: +# license: MIT +# function: dashboard +# relpath: server/routes.py +# provenance: human +# mechanism: source_reaches_sink + +def dashboard(): + # Check if the user is logged in + if 'username' in session: + + search_query = request.args.get('search', '') + + # Get user_id from the session username + with get_db_connection() as conn: + cursor = conn.cursor() + query = "SELECT id FROM users WHERE username = ?" + cursor.execute(query, (session['username'],)) + user = cursor.fetchone() + user_id = user['id'] + + if search_query: + s_query = f"SELECT filename FROM files WHERE user_id = {user_id} AND filename LIKE '%{search_query}%'" + cursor.execute(s_query) + else: + cursor.execute("SELECT filename FROM files WHERE user_id = ?", (user_id,)) + files = [row['filename'] for row in cursor.fetchall()] + + return render_template('dashboard.html', files=files, search_query=search_query) + else: + return redirect(url_for('login')) diff --git a/benchmarks/pairs/vibe-py/training/manifest.jsonl b/benchmarks/pairs/vibe-py/training/manifest.jsonl new file mode 100644 index 0000000..f45416b --- /dev/null +++ b/benchmarks/pairs/vibe-py/training/manifest.jsonl @@ -0,0 +1,70 @@ +{"class": "command_injection", "commit": "a961308c02d1fb462b192681c336b0739e432da7", "commit_url": "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L211", "cve": "", "cwe": "CWE-78", "file": "damn-vulnerable-graphql-application-views-mutate/vuln.py", "function": "mutate", "id": "damn-vulnerable-graphql-application-views-mutate:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "damn-vulnerable-graphql-application-views-mutate", "project": "realvuln-damn-vulnerable-graphql-application", "provenance": "human", "relpath": "core/views.py", "repo": "dolevf/Damn-Vulnerable-GraphQL-Application", "split": "train", "year": 2026} +{"class": "command_injection", "commit": "a961308c02d1fb462b192681c336b0739e432da7", "commit_url": "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L211", "cve": "", "cwe": "CWE-78", "file": "damn-vulnerable-graphql-application-views-mutate/fixed.py", "function": "mutate", "id": "damn-vulnerable-graphql-application-views-mutate:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "damn-vulnerable-graphql-application-views-mutate", "project": "realvuln-damn-vulnerable-graphql-application", "provenance": "human", "relpath": "core/views.py", "repo": "dolevf/Damn-Vulnerable-GraphQL-Application", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "a961308c02d1fb462b192681c336b0739e432da7", "commit_url": "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L320", "cve": "", "cwe": "CWE-89", "file": "damn-vulnerable-graphql-application-views-resolve-pastes/vuln.py", "function": "resolve_pastes", "id": "damn-vulnerable-graphql-application-views-resolve-pastes:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "damn-vulnerable-graphql-application-views-resolve-pastes", "project": "realvuln-damn-vulnerable-graphql-application", "provenance": "human", "relpath": "core/views.py", "repo": "dolevf/Damn-Vulnerable-GraphQL-Application", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "a961308c02d1fb462b192681c336b0739e432da7", "commit_url": "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L320", "cve": "", "cwe": "CWE-89", "file": "damn-vulnerable-graphql-application-views-resolve-pastes/fixed.py", "function": "resolve_pastes", "id": "damn-vulnerable-graphql-application-views-resolve-pastes:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "damn-vulnerable-graphql-application-views-resolve-pastes", "project": "realvuln-damn-vulnerable-graphql-application", "provenance": "human", "relpath": "core/views.py", "repo": "dolevf/Damn-Vulnerable-GraphQL-Application", "split": "holdout", "year": 2026} +{"class": "command_injection", "commit": "a961308c02d1fb462b192681c336b0739e432da7", "commit_url": "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L345", "cve": "", "cwe": "CWE-78", "file": "damn-vulnerable-graphql-application-views-resolve-system-dia/vuln.py", "function": "resolve_system_diagnostics", "id": "damn-vulnerable-graphql-application-views-resolve-system-dia:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "damn-vulnerable-graphql-application-views-resolve-system-dia", "project": "realvuln-damn-vulnerable-graphql-application", "provenance": "human", "relpath": "core/views.py", "repo": "dolevf/Damn-Vulnerable-GraphQL-Application", "split": "train", "year": 2026} +{"class": "command_injection", "commit": "a961308c02d1fb462b192681c336b0739e432da7", "commit_url": "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/a961308c02d1fb462b192681c336b0739e432da7/core/views.py#L345", "cve": "", "cwe": "CWE-78", "file": "damn-vulnerable-graphql-application-views-resolve-system-dia/fixed.py", "function": "resolve_system_diagnostics", "id": "damn-vulnerable-graphql-application-views-resolve-system-dia:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "damn-vulnerable-graphql-application-views-resolve-system-dia", "project": "realvuln-damn-vulnerable-graphql-application", "provenance": "human", "relpath": "core/views.py", "repo": "dolevf/Damn-Vulnerable-GraphQL-Application", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "082c88bfefced508ae083b92b459c808f95e905e", "commit_url": "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/models/Analytics/analytics.py#L57", "cve": "", "cwe": "CWE-89", "file": "djangoat-analytics-hits-by-ip/vuln.py", "function": "hits_by_ip", "id": "djangoat-analytics-hits-by-ip:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "djangoat-analytics-hits-by-ip", "project": "realvuln-djangoat", "provenance": "human", "relpath": "app/models/Analytics/analytics.py", "repo": "Contrast-Security-OSS/DjanGoat", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "082c88bfefced508ae083b92b459c808f95e905e", "commit_url": "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/models/Analytics/analytics.py#L57", "cve": "", "cwe": "CWE-89", "file": "djangoat-analytics-hits-by-ip/fixed.py", "function": "hits_by_ip", "id": "djangoat-analytics-hits-by-ip:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "djangoat-analytics-hits-by-ip", "project": "realvuln-djangoat", "provenance": "human", "relpath": "app/models/Analytics/analytics.py", "repo": "Contrast-Security-OSS/DjanGoat", "split": "holdout", "year": 2026} +{"class": "insecure_deserialization", "commit": "082c88bfefced508ae083b92b459c808f95e905e", "commit_url": "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/password_resets/views.py#L84", "cve": "", "cwe": "CWE-502", "file": "djangoat-views-reset-password/vuln.py", "function": "reset_password", "id": "djangoat-views-reset-password:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "djangoat-views-reset-password", "project": "realvuln-djangoat", "provenance": "human", "relpath": "app/views/password_resets/views.py", "repo": "Contrast-Security-OSS/DjanGoat", "split": "train", "year": 2026} +{"class": "insecure_deserialization", "commit": "082c88bfefced508ae083b92b459c808f95e905e", "commit_url": "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/password_resets/views.py#L84", "cve": "", "cwe": "CWE-502", "file": "djangoat-views-reset-password/fixed.py", "function": "reset_password", "id": "djangoat-views-reset-password:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "djangoat-views-reset-password", "project": "realvuln-djangoat", "provenance": "human", "relpath": "app/views/password_resets/views.py", "repo": "Contrast-Security-OSS/DjanGoat", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "082c88bfefced508ae083b92b459c808f95e905e", "commit_url": "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/users/views.py#L74", "cve": "", "cwe": "CWE-89", "file": "djangoat-views-user-view/vuln.py", "function": "user_view", "id": "djangoat-views-user-view:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "djangoat-views-user-view", "project": "realvuln-djangoat", "provenance": "human", "relpath": "app/views/users/views.py", "repo": "Contrast-Security-OSS/DjanGoat", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "082c88bfefced508ae083b92b459c808f95e905e", "commit_url": "https://github.com/Contrast-Security-OSS/DjanGoat/blob/082c88bfefced508ae083b92b459c808f95e905e/app/views/users/views.py#L74", "cve": "", "cwe": "CWE-89", "file": "djangoat-views-user-view/fixed.py", "function": "user_view", "id": "djangoat-views-user-view:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "djangoat-views-user-view", "project": "realvuln-djangoat", "provenance": "human", "relpath": "app/views/users/views.py", "repo": "Contrast-Security-OSS/DjanGoat", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "a06abb306ed3699bb2f073e3f41d3d9c57012819", "commit_url": "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/attacks.py#L39", "cve": "", "cwe": "CWE-89", "file": "dsvpwa-attacks-run/vuln.py", "function": "run", "id": "dsvpwa-attacks-run:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dsvpwa-attacks-run", "project": "realvuln-dsvpwa", "provenance": "human", "relpath": "dsvpwa/attacks.py", "repo": "sgabe/DSVPWA", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "a06abb306ed3699bb2f073e3f41d3d9c57012819", "commit_url": "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/attacks.py#L39", "cve": "", "cwe": "CWE-89", "file": "dsvpwa-attacks-run/fixed.py", "function": "run", "id": "dsvpwa-attacks-run:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dsvpwa-attacks-run", "project": "realvuln-dsvpwa", "provenance": "human", "relpath": "dsvpwa/attacks.py", "repo": "sgabe/DSVPWA", "split": "train", "year": 2026} +{"class": "security_misconfiguration", "commit": "a06abb306ed3699bb2f073e3f41d3d9c57012819", "commit_url": "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa.py#L37", "cve": "", "cwe": "CWE-326", "file": "dsvpwa-dsvpwa-main/vuln.py", "function": "main", "id": "dsvpwa-dsvpwa-main:vuln", "label": "vuln", "license": "MIT", "mechanism": "permissive_default", "pair": "dsvpwa-dsvpwa-main", "project": "realvuln-dsvpwa", "provenance": "human", "relpath": "dsvpwa.py", "repo": "sgabe/DSVPWA", "split": "holdout", "year": 2026} +{"class": "security_misconfiguration", "commit": "a06abb306ed3699bb2f073e3f41d3d9c57012819", "commit_url": "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa.py#L37", "cve": "", "cwe": "CWE-326", "file": "dsvpwa-dsvpwa-main/fixed.py", "function": "main", "id": "dsvpwa-dsvpwa-main:fixed", "label": "fixed", "license": "MIT", "mechanism": "permissive_default", "pair": "dsvpwa-dsvpwa-main", "project": "realvuln-dsvpwa", "provenance": "human", "relpath": "dsvpwa.py", "repo": "sgabe/DSVPWA", "split": "holdout", "year": 2026} +{"class": "command_injection", "commit": "a06abb306ed3699bb2f073e3f41d3d9c57012819", "commit_url": "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/handlers.py#L183", "cve": "", "cwe": "CWE-78", "file": "dsvpwa-handlers-do-bdr/vuln.py", "function": "do_BDR", "id": "dsvpwa-handlers-do-bdr:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dsvpwa-handlers-do-bdr", "project": "realvuln-dsvpwa", "provenance": "human", "relpath": "dsvpwa/handlers.py", "repo": "sgabe/DSVPWA", "split": "train", "year": 2026} +{"class": "command_injection", "commit": "a06abb306ed3699bb2f073e3f41d3d9c57012819", "commit_url": "https://github.com/sgabe/DSVPWA/blob/a06abb306ed3699bb2f073e3f41d3d9c57012819/dsvpwa/handlers.py#L183", "cve": "", "cwe": "CWE-78", "file": "dsvpwa-handlers-do-bdr/fixed.py", "function": "do_BDR", "id": "dsvpwa-handlers-do-bdr:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dsvpwa-handlers-do-bdr", "project": "realvuln-dsvpwa", "provenance": "human", "relpath": "dsvpwa/handlers.py", "repo": "sgabe/DSVPWA", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "7d40f4b7939c901610ed9b85724552d60e7d63fa", "commit_url": "https://github.com/stamparm/DSVW/blob/7d40f4b7939c901610ed9b85724552d60e7d63fa/dsvw.py#L30", "cve": "", "cwe": "CWE-89", "file": "dsvw-dsvw-do-get/vuln.py", "function": "do_GET", "id": "dsvw-dsvw-do-get:vuln", "label": "vuln", "license": "Unlicense", "mechanism": "source_reaches_sink", "pair": "dsvw-dsvw-do-get", "project": "realvuln-dsvw", "provenance": "human", "relpath": "dsvw.py", "repo": "stamparm/DSVW", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "7d40f4b7939c901610ed9b85724552d60e7d63fa", "commit_url": "https://github.com/stamparm/DSVW/blob/7d40f4b7939c901610ed9b85724552d60e7d63fa/dsvw.py#L30", "cve": "", "cwe": "CWE-89", "file": "dsvw-dsvw-do-get/fixed.py", "function": "do_GET", "id": "dsvw-dsvw-do-get:fixed", "label": "fixed", "license": "Unlicense", "mechanism": "source_reaches_sink", "pair": "dsvw-dsvw-do-get", "project": "realvuln-dsvw", "provenance": "human", "relpath": "dsvw.py", "repo": "stamparm/DSVW", "split": "holdout", "year": 2026} +{"class": "security_misconfiguration", "commit": "a1d8f89fac2e57093189853c6527c2b01fc1d9c1", "commit_url": "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/app.py#L35", "cve": "", "cwe": "CWE-16", "file": "dvpwa-app-init/vuln.py", "function": "init", "id": "dvpwa-app-init:vuln", "label": "vuln", "license": "MIT", "mechanism": "permissive_default", "pair": "dvpwa-app-init", "project": "realvuln-dvpwa", "provenance": "human", "relpath": "sqli/app.py", "repo": "anxolerd/dvpwa", "split": "train", "year": 2026} +{"class": "security_misconfiguration", "commit": "a1d8f89fac2e57093189853c6527c2b01fc1d9c1", "commit_url": "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/app.py#L35", "cve": "", "cwe": "CWE-16", "file": "dvpwa-app-init/fixed.py", "function": "init", "id": "dvpwa-app-init:fixed", "label": "fixed", "license": "MIT", "mechanism": "permissive_default", "pair": "dvpwa-app-init", "project": "realvuln-dvpwa", "provenance": "human", "relpath": "sqli/app.py", "repo": "anxolerd/dvpwa", "split": "train", "year": 2026} +{"class": "security_misconfiguration", "commit": "a1d8f89fac2e57093189853c6527c2b01fc1d9c1", "commit_url": "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/middlewares.py#L20", "cve": "", "cwe": "CWE-1004", "file": "dvpwa-middlewares-session-middleware/vuln.py", "function": "session_middleware", "id": "dvpwa-middlewares-session-middleware:vuln", "label": "vuln", "license": "MIT", "mechanism": "permissive_default", "pair": "dvpwa-middlewares-session-middleware", "project": "realvuln-dvpwa", "provenance": "human", "relpath": "sqli/middlewares.py", "repo": "anxolerd/dvpwa", "split": "holdout", "year": 2026} +{"class": "security_misconfiguration", "commit": "a1d8f89fac2e57093189853c6527c2b01fc1d9c1", "commit_url": "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/middlewares.py#L20", "cve": "", "cwe": "CWE-1004", "file": "dvpwa-middlewares-session-middleware/fixed.py", "function": "session_middleware", "id": "dvpwa-middlewares-session-middleware:fixed", "label": "fixed", "license": "MIT", "mechanism": "permissive_default", "pair": "dvpwa-middlewares-session-middleware", "project": "realvuln-dvpwa", "provenance": "human", "relpath": "sqli/middlewares.py", "repo": "anxolerd/dvpwa", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "a1d8f89fac2e57093189853c6527c2b01fc1d9c1", "commit_url": "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/dao/student.py#L42", "cve": "", "cwe": "CWE-89", "file": "dvpwa-student-create/vuln.py", "function": "create", "id": "dvpwa-student-create:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dvpwa-student-create", "project": "realvuln-dvpwa", "provenance": "human", "relpath": "sqli/dao/student.py", "repo": "anxolerd/dvpwa", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "a1d8f89fac2e57093189853c6527c2b01fc1d9c1", "commit_url": "https://github.com/anxolerd/dvpwa/blob/a1d8f89fac2e57093189853c6527c2b01fc1d9c1/sqli/dao/student.py#L42", "cve": "", "cwe": "CWE-89", "file": "dvpwa-student-create/fixed.py", "function": "create", "id": "dvpwa-student-create:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "dvpwa-student-create", "project": "realvuln-dvpwa", "provenance": "human", "relpath": "sqli/dao/student.py", "repo": "anxolerd/dvpwa", "split": "train", "year": 2026} +{"class": "insecure_deserialization", "commit": "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa", "commit_url": "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L118", "cve": "", "cwe": "CWE-502", "file": "extremely-vulnerable-flask-app-account-before-request/vuln.py", "function": "before_request", "id": "extremely-vulnerable-flask-app-account-before-request:vuln", "label": "vuln", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "extremely-vulnerable-flask-app-account-before-request", "project": "realvuln-extremely-vulnerable-flask-app", "provenance": "human", "relpath": "routes/account.py", "repo": "manuelz120/extremely-vulnerable-flask-app", "split": "holdout", "year": 2026} +{"class": "insecure_deserialization", "commit": "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa", "commit_url": "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L118", "cve": "", "cwe": "CWE-502", "file": "extremely-vulnerable-flask-app-account-before-request/fixed.py", "function": "before_request", "id": "extremely-vulnerable-flask-app-account-before-request:fixed", "label": "fixed", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "extremely-vulnerable-flask-app-account-before-request", "project": "realvuln-extremely-vulnerable-flask-app", "provenance": "human", "relpath": "routes/account.py", "repo": "manuelz120/extremely-vulnerable-flask-app", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa", "commit_url": "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L33", "cve": "", "cwe": "CWE-89", "file": "extremely-vulnerable-flask-app-account-search/vuln.py", "function": "search", "id": "extremely-vulnerable-flask-app-account-search:vuln", "label": "vuln", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "extremely-vulnerable-flask-app-account-search", "project": "realvuln-extremely-vulnerable-flask-app", "provenance": "human", "relpath": "routes/account.py", "repo": "manuelz120/extremely-vulnerable-flask-app", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa", "commit_url": "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/account.py#L33", "cve": "", "cwe": "CWE-89", "file": "extremely-vulnerable-flask-app-account-search/fixed.py", "function": "search", "id": "extremely-vulnerable-flask-app-account-search:fixed", "label": "fixed", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "extremely-vulnerable-flask-app-account-search", "project": "realvuln-extremely-vulnerable-flask-app", "provenance": "human", "relpath": "routes/account.py", "repo": "manuelz120/extremely-vulnerable-flask-app", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa", "commit_url": "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/signup.py#L15", "cve": "", "cwe": "CWE-89", "file": "extremely-vulnerable-flask-app-signup-validate-token/vuln.py", "function": "validate_token", "id": "extremely-vulnerable-flask-app-signup-validate-token:vuln", "label": "vuln", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "extremely-vulnerable-flask-app-signup-validate-token", "project": "realvuln-extremely-vulnerable-flask-app", "provenance": "human", "relpath": "routes/signup.py", "repo": "manuelz120/extremely-vulnerable-flask-app", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "d5d8875559e21222bbbaadb7b9f0af592c6eb7fa", "commit_url": "https://github.com/manuelz120/extremely-vulnerable-flask-app/blob/d5d8875559e21222bbbaadb7b9f0af592c6eb7fa/routes/signup.py#L15", "cve": "", "cwe": "CWE-89", "file": "extremely-vulnerable-flask-app-signup-validate-token/fixed.py", "function": "validate_token", "id": "extremely-vulnerable-flask-app-signup-validate-token:fixed", "label": "fixed", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "extremely-vulnerable-flask-app-signup-validate-token", "project": "realvuln-extremely-vulnerable-flask-app", "provenance": "human", "relpath": "routes/signup.py", "repo": "manuelz120/extremely-vulnerable-flask-app", "split": "holdout", "year": 2026} +{"class": "ssti", "commit": "80b894a2f754590747a31c92b2088be086421953", "commit_url": "https://github.com/kolega-ai/realvuln-python-insecure-app/blob/80b894a2f754590747a31c92b2088be086421953/app/main.py#L41", "cve": "", "cwe": "CWE-1336", "file": "python-insecure-app-main-try-hack-me/vuln.py", "function": "try_hack_me", "id": "python-insecure-app-main-try-hack-me:vuln", "label": "vuln", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "python-insecure-app-main-try-hack-me", "project": "realvuln-python-insecure-app", "provenance": "human", "relpath": "app/main.py", "repo": "kolega-ai/realvuln-python-insecure-app", "split": "train", "year": 2026} +{"class": "ssti", "commit": "80b894a2f754590747a31c92b2088be086421953", "commit_url": "https://github.com/kolega-ai/realvuln-python-insecure-app/blob/80b894a2f754590747a31c92b2088be086421953/app/main.py#L41", "cve": "", "cwe": "CWE-1336", "file": "python-insecure-app-main-try-hack-me/fixed.py", "function": "try_hack_me", "id": "python-insecure-app-main-try-hack-me:fixed", "label": "fixed", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "python-insecure-app-main-try-hack-me", "project": "realvuln-python-insecure-app", "provenance": "human", "relpath": "app/main.py", "repo": "kolega-ai/realvuln-python-insecure-app", "split": "train", "year": 2026} +{"class": "reflected_xss", "commit": "76339c27d5a870c0a81bad6aee5f9bd516592145", "commit_url": "https://github.com/TheWation/PythonSSTI/blob/76339c27d5a870c0a81bad6aee5f9bd516592145/main.py#L14", "cve": "", "cwe": "CWE-79", "file": "pythonssti-main-read-root/vuln.py", "function": "read_root", "id": "pythonssti-main-read-root:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "pythonssti-main-read-root", "project": "realvuln-pythonssti", "provenance": "human", "relpath": "main.py", "repo": "TheWation/PythonSSTI", "split": "holdout", "year": 2026} +{"class": "reflected_xss", "commit": "76339c27d5a870c0a81bad6aee5f9bd516592145", "commit_url": "https://github.com/TheWation/PythonSSTI/blob/76339c27d5a870c0a81bad6aee5f9bd516592145/main.py#L14", "cve": "", "cwe": "CWE-79", "file": "pythonssti-main-read-root/fixed.py", "function": "read_root", "id": "pythonssti-main-read-root:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "pythonssti-main-read-root", "project": "realvuln-pythonssti", "provenance": "human", "relpath": "main.py", "repo": "TheWation/PythonSSTI", "split": "holdout", "year": 2026} +{"class": "broken_access_control", "commit": "c4db46f72586c4ecb65859051ea037018dbf0e5e", "commit_url": "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L223", "cve": "", "cwe": "CWE-862", "file": "threatbyte-api-v1-delete/vuln.py", "function": "delete", "id": "threatbyte-api-v1-delete:vuln", "label": "vuln", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "threatbyte-api-v1-delete", "project": "realvuln-threatbyte", "provenance": "human", "relpath": "server/api/v1/api_v1.py", "repo": "anotherik/ThreatByte", "split": "train", "year": 2026} +{"class": "broken_access_control", "commit": "c4db46f72586c4ecb65859051ea037018dbf0e5e", "commit_url": "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L223", "cve": "", "cwe": "CWE-862", "file": "threatbyte-api-v1-delete/fixed.py", "function": "delete", "id": "threatbyte-api-v1-delete:fixed", "label": "fixed", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "threatbyte-api-v1-delete", "project": "realvuln-threatbyte", "provenance": "human", "relpath": "server/api/v1/api_v1.py", "repo": "anotherik/ThreatByte", "split": "train", "year": 2026} +{"class": "broken_access_control", "commit": "c4db46f72586c4ecb65859051ea037018dbf0e5e", "commit_url": "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L154", "cve": "", "cwe": "CWE-639", "file": "threatbyte-api-v1-get/vuln.py", "function": "get", "id": "threatbyte-api-v1-get:vuln", "label": "vuln", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "threatbyte-api-v1-get", "project": "realvuln-threatbyte", "provenance": "human", "relpath": "server/api/v1/api_v1.py", "repo": "anotherik/ThreatByte", "split": "holdout", "year": 2026} +{"class": "broken_access_control", "commit": "c4db46f72586c4ecb65859051ea037018dbf0e5e", "commit_url": "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/api/v1/api_v1.py#L154", "cve": "", "cwe": "CWE-639", "file": "threatbyte-api-v1-get/fixed.py", "function": "get", "id": "threatbyte-api-v1-get:fixed", "label": "fixed", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "threatbyte-api-v1-get", "project": "realvuln-threatbyte", "provenance": "human", "relpath": "server/api/v1/api_v1.py", "repo": "anotherik/ThreatByte", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "c4db46f72586c4ecb65859051ea037018dbf0e5e", "commit_url": "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/routes.py#L137", "cve": "", "cwe": "CWE-89", "file": "threatbyte-routes-dashboard/vuln.py", "function": "dashboard", "id": "threatbyte-routes-dashboard:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "threatbyte-routes-dashboard", "project": "realvuln-threatbyte", "provenance": "human", "relpath": "server/routes.py", "repo": "anotherik/ThreatByte", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "c4db46f72586c4ecb65859051ea037018dbf0e5e", "commit_url": "https://github.com/anotherik/ThreatByte/blob/c4db46f72586c4ecb65859051ea037018dbf0e5e/server/routes.py#L137", "cve": "", "cwe": "CWE-89", "file": "threatbyte-routes-dashboard/fixed.py", "function": "dashboard", "id": "threatbyte-routes-dashboard:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "threatbyte-routes-dashboard", "project": "realvuln-threatbyte", "provenance": "human", "relpath": "server/routes.py", "repo": "anotherik/ThreatByte", "split": "train", "year": 2026} +{"class": "broken_access_control", "commit": "1713b54b601ad29582581eeda4b31fceb1319874", "commit_url": "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51", "cve": "", "cwe": "CWE-639", "file": "vampi-books-get-by-title/vuln.py", "function": "get_by_title", "id": "vampi-books-get-by-title:vuln", "label": "vuln", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "vampi-books-get-by-title", "project": "realvuln-vampi", "provenance": "human", "relpath": "api_views/books.py", "repo": "erev0s/VAmPI", "split": "holdout", "year": 2026} +{"class": "broken_access_control", "commit": "1713b54b601ad29582581eeda4b31fceb1319874", "commit_url": "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51", "cve": "", "cwe": "CWE-639", "file": "vampi-books-get-by-title/fixed.py", "function": "get_by_title", "id": "vampi-books-get-by-title:fixed", "label": "fixed", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "vampi-books-get-by-title", "project": "realvuln-vampi", "provenance": "human", "relpath": "api_views/books.py", "repo": "erev0s/VAmPI", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "1713b54b601ad29582581eeda4b31fceb1319874", "commit_url": "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/models/user_model.py#L72", "cve": "", "cwe": "CWE-89", "file": "vampi-user-model-get-user/vuln.py", "function": "get_user", "id": "vampi-user-model-get-user:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vampi-user-model-get-user", "project": "realvuln-vampi", "provenance": "human", "relpath": "models/user_model.py", "repo": "erev0s/VAmPI", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "1713b54b601ad29582581eeda4b31fceb1319874", "commit_url": "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/models/user_model.py#L72", "cve": "", "cwe": "CWE-89", "file": "vampi-user-model-get-user/fixed.py", "function": "get_user", "id": "vampi-user-model-get-user:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vampi-user-model-get-user", "project": "realvuln-vampi", "provenance": "human", "relpath": "models/user_model.py", "repo": "erev0s/VAmPI", "split": "train", "year": 2026} +{"class": "broken_access_control", "commit": "1713b54b601ad29582581eeda4b31fceb1319874", "commit_url": "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187", "cve": "", "cwe": "CWE-639", "file": "vampi-users-update-password/vuln.py", "function": "update_password", "id": "vampi-users-update-password:vuln", "label": "vuln", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "vampi-users-update-password", "project": "realvuln-vampi", "provenance": "human", "relpath": "api_views/users.py", "repo": "erev0s/VAmPI", "split": "holdout", "year": 2026} +{"class": "broken_access_control", "commit": "1713b54b601ad29582581eeda4b31fceb1319874", "commit_url": "https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187", "cve": "", "cwe": "CWE-639", "file": "vampi-users-update-password/fixed.py", "function": "update_password", "id": "vampi-users-update-password:fixed", "label": "fixed", "license": "MIT", "mechanism": "missing_auth_guard", "pair": "vampi-users-update-password", "project": "realvuln-vampi", "provenance": "human", "relpath": "api_views/users.py", "repo": "erev0s/VAmPI", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3", "commit_url": "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L231", "cve": "", "cwe": "CWE-89", "file": "vfapi-main-delete-user/vuln.py", "function": "delete_user", "id": "vfapi-main-delete-user:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vfapi-main-delete-user", "project": "realvuln-vfapi", "provenance": "human", "relpath": "main.py", "repo": "naryal2580/vfapi", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3", "commit_url": "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L231", "cve": "", "cwe": "CWE-89", "file": "vfapi-main-delete-user/fixed.py", "function": "delete_user", "id": "vfapi-main-delete-user:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vfapi-main-delete-user", "project": "realvuln-vfapi", "provenance": "human", "relpath": "main.py", "repo": "naryal2580/vfapi", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3", "commit_url": "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L188", "cve": "", "cwe": "CWE-89", "file": "vfapi-main-put-user/vuln.py", "function": "put_user", "id": "vfapi-main-put-user:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vfapi-main-put-user", "project": "realvuln-vfapi", "provenance": "human", "relpath": "main.py", "repo": "naryal2580/vfapi", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3", "commit_url": "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L188", "cve": "", "cwe": "CWE-89", "file": "vfapi-main-put-user/fixed.py", "function": "put_user", "id": "vfapi-main-put-user:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vfapi-main-put-user", "project": "realvuln-vfapi", "provenance": "human", "relpath": "main.py", "repo": "naryal2580/vfapi", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3", "commit_url": "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L182", "cve": "", "cwe": "CWE-89", "file": "vfapi-main-sql-return-users-from-username/vuln.py", "function": "sql_return_users_from_username", "id": "vfapi-main-sql-return-users-from-username:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vfapi-main-sql-return-users-from-username", "project": "realvuln-vfapi", "provenance": "human", "relpath": "main.py", "repo": "naryal2580/vfapi", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3", "commit_url": "https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L182", "cve": "", "cwe": "CWE-89", "file": "vfapi-main-sql-return-users-from-username/fixed.py", "function": "sql_return_users_from_username", "id": "vfapi-main-sql-return-users-from-username:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vfapi-main-sql-return-users-from-username", "project": "realvuln-vfapi", "provenance": "human", "relpath": "main.py", "repo": "naryal2580/vfapi", "split": "train", "year": 2026} +{"class": "reflected_xss", "commit": "6336c77e6db523a5df34c4b628dff4159110797a", "commit_url": "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Insecure_Frameworks/bad_mvc.py#L37", "cve": "", "cwe": "CWE-79", "file": "vulnerable-python-apps-bad-mvc-do-get/vuln.py", "function": "do_GET", "id": "vulnerable-python-apps-bad-mvc-do-get:vuln", "label": "vuln", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "vulnerable-python-apps-bad-mvc-do-get", "project": "realvuln-vulnerable-python-apps", "provenance": "human", "relpath": "Insecure_Frameworks/bad_mvc.py", "repo": "LinuxUser255/Vulnerable_Python_Apps", "split": "holdout", "year": 2026} +{"class": "reflected_xss", "commit": "6336c77e6db523a5df34c4b628dff4159110797a", "commit_url": "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Insecure_Frameworks/bad_mvc.py#L37", "cve": "", "cwe": "CWE-79", "file": "vulnerable-python-apps-bad-mvc-do-get/fixed.py", "function": "do_GET", "id": "vulnerable-python-apps-bad-mvc-do-get:fixed", "label": "fixed", "license": "GPL-3.0", "mechanism": "source_reaches_sink", "pair": "vulnerable-python-apps-bad-mvc-do-get", "project": "realvuln-vulnerable-python-apps", "provenance": "human", "relpath": "Insecure_Frameworks/bad_mvc.py", "repo": "LinuxUser255/Vulnerable_Python_Apps", "split": "holdout", "year": 2026} +{"class": "path_traversal", "commit": "6336c77e6db523a5df34c4b628dff4159110797a", "commit_url": "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Directory_Traversal/Dir_Traversal.py#L22", "cve": "", "cwe": "CWE-22", "file": "vulnerable-python-apps-dir-traversal-do-get/vuln.py", "function": "do_GET", "id": "vulnerable-python-apps-dir-traversal-do-get:vuln", "label": "vuln", "license": "GPL-3.0", "mechanism": "path_join_user_input", "pair": "vulnerable-python-apps-dir-traversal-do-get", "project": "realvuln-vulnerable-python-apps", "provenance": "human", "relpath": "Directory_Traversal/Dir_Traversal.py", "repo": "LinuxUser255/Vulnerable_Python_Apps", "split": "train", "year": 2026} +{"class": "path_traversal", "commit": "6336c77e6db523a5df34c4b628dff4159110797a", "commit_url": "https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Directory_Traversal/Dir_Traversal.py#L22", "cve": "", "cwe": "CWE-22", "file": "vulnerable-python-apps-dir-traversal-do-get/fixed.py", "function": "do_GET", "id": "vulnerable-python-apps-dir-traversal-do-get:fixed", "label": "fixed", "license": "GPL-3.0", "mechanism": "path_join_user_input", "pair": "vulnerable-python-apps-dir-traversal-do-get", "project": "realvuln-vulnerable-python-apps", "provenance": "human", "relpath": "Directory_Traversal/Dir_Traversal.py", "repo": "LinuxUser255/Vulnerable_Python_Apps", "split": "train", "year": 2026} +{"class": "command_injection", "commit": "d1d0f64c06f038fe59a049e0e6001bfaf9839697", "commit_url": "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L6", "cve": "", "cwe": "CWE-78", "file": "vulnpy-cmdi-do-os-system/vuln.py", "function": "do_os_system", "id": "vulnpy-cmdi-do-os-system:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulnpy-cmdi-do-os-system", "project": "realvuln-vulnpy", "provenance": "human", "relpath": "src/vulnpy/trigger/cmdi.py", "repo": "Contrast-Security-OSS/vulnpy", "split": "train", "year": 2026} +{"class": "command_injection", "commit": "d1d0f64c06f038fe59a049e0e6001bfaf9839697", "commit_url": "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L6", "cve": "", "cwe": "CWE-78", "file": "vulnpy-cmdi-do-os-system/fixed.py", "function": "do_os_system", "id": "vulnpy-cmdi-do-os-system:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulnpy-cmdi-do-os-system", "project": "realvuln-vulnpy", "provenance": "human", "relpath": "src/vulnpy/trigger/cmdi.py", "repo": "Contrast-Security-OSS/vulnpy", "split": "train", "year": 2026} +{"class": "command_injection", "commit": "d1d0f64c06f038fe59a049e0e6001bfaf9839697", "commit_url": "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L10", "cve": "", "cwe": "CWE-78", "file": "vulnpy-cmdi-do-subprocess-popen/vuln.py", "function": "do_subprocess_popen", "id": "vulnpy-cmdi-do-subprocess-popen:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulnpy-cmdi-do-subprocess-popen", "project": "realvuln-vulnpy", "provenance": "human", "relpath": "src/vulnpy/trigger/cmdi.py", "repo": "Contrast-Security-OSS/vulnpy", "split": "holdout", "year": 2026} +{"class": "command_injection", "commit": "d1d0f64c06f038fe59a049e0e6001bfaf9839697", "commit_url": "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L10", "cve": "", "cwe": "CWE-78", "file": "vulnpy-cmdi-do-subprocess-popen/fixed.py", "function": "do_subprocess_popen", "id": "vulnpy-cmdi-do-subprocess-popen:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulnpy-cmdi-do-subprocess-popen", "project": "realvuln-vulnpy", "provenance": "human", "relpath": "src/vulnpy/trigger/cmdi.py", "repo": "Contrast-Security-OSS/vulnpy", "split": "holdout", "year": 2026} +{"class": "insecure_deserialization", "commit": "d1d0f64c06f038fe59a049e0e6001bfaf9839697", "commit_url": "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/deserialization.py#L8", "cve": "", "cwe": "CWE-502", "file": "vulnpy-deserialization-do-pickle-load/vuln.py", "function": "do_pickle_load", "id": "vulnpy-deserialization-do-pickle-load:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulnpy-deserialization-do-pickle-load", "project": "realvuln-vulnpy", "provenance": "human", "relpath": "src/vulnpy/trigger/deserialization.py", "repo": "Contrast-Security-OSS/vulnpy", "split": "train", "year": 2026} +{"class": "insecure_deserialization", "commit": "d1d0f64c06f038fe59a049e0e6001bfaf9839697", "commit_url": "https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/deserialization.py#L8", "cve": "", "cwe": "CWE-502", "file": "vulnpy-deserialization-do-pickle-load/fixed.py", "function": "do_pickle_load", "id": "vulnpy-deserialization-do-pickle-load:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulnpy-deserialization-do-pickle-load", "project": "realvuln-vulnpy", "provenance": "human", "relpath": "src/vulnpy/trigger/deserialization.py", "repo": "Contrast-Security-OSS/vulnpy", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "5249cc8b05a1c37f6b2f757b1cf16a509c327122", "commit_url": "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L25", "cve": "", "cwe": "CWE-89", "file": "vulpy-libuser-create/vuln.py", "function": "create", "id": "vulpy-libuser-create:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulpy-libuser-create", "project": "realvuln-vulpy", "provenance": "human", "relpath": "bad/libuser.py", "repo": "fportantier/vulpy", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "5249cc8b05a1c37f6b2f757b1cf16a509c327122", "commit_url": "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L25", "cve": "", "cwe": "CWE-89", "file": "vulpy-libuser-create/fixed.py", "function": "create", "id": "vulpy-libuser-create:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulpy-libuser-create", "project": "realvuln-vulpy", "provenance": "human", "relpath": "bad/libuser.py", "repo": "fportantier/vulpy", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "5249cc8b05a1c37f6b2f757b1cf16a509c327122", "commit_url": "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L12", "cve": "", "cwe": "CWE-89", "file": "vulpy-libuser-login/vuln.py", "function": "login", "id": "vulpy-libuser-login:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulpy-libuser-login", "project": "realvuln-vulpy", "provenance": "human", "relpath": "bad/libuser.py", "repo": "fportantier/vulpy", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "5249cc8b05a1c37f6b2f757b1cf16a509c327122", "commit_url": "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L12", "cve": "", "cwe": "CWE-89", "file": "vulpy-libuser-login/fixed.py", "function": "login", "id": "vulpy-libuser-login:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulpy-libuser-login", "project": "realvuln-vulpy", "provenance": "human", "relpath": "bad/libuser.py", "repo": "fportantier/vulpy", "split": "train", "year": 2026} +{"class": "sql_injection", "commit": "5249cc8b05a1c37f6b2f757b1cf16a509c327122", "commit_url": "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L53", "cve": "", "cwe": "CWE-89", "file": "vulpy-libuser-password-change/vuln.py", "function": "password_change", "id": "vulpy-libuser-password-change:vuln", "label": "vuln", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulpy-libuser-password-change", "project": "realvuln-vulpy", "provenance": "human", "relpath": "bad/libuser.py", "repo": "fportantier/vulpy", "split": "holdout", "year": 2026} +{"class": "sql_injection", "commit": "5249cc8b05a1c37f6b2f757b1cf16a509c327122", "commit_url": "https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L53", "cve": "", "cwe": "CWE-89", "file": "vulpy-libuser-password-change/fixed.py", "function": "password_change", "id": "vulpy-libuser-password-change:fixed", "label": "fixed", "license": "MIT", "mechanism": "source_reaches_sink", "pair": "vulpy-libuser-password-change", "project": "realvuln-vulpy", "provenance": "human", "relpath": "bad/libuser.py", "repo": "fportantier/vulpy", "split": "holdout", "year": 2026} diff --git a/benchmarks/pairs/vibe-py/vampi-books-get-by-title/fixed.py b/benchmarks/pairs/vibe-py/vampi-books-get-by-title/fixed.py new file mode 100644 index 0000000..684ea98 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vampi-books-get-by-title/fixed.py @@ -0,0 +1,40 @@ +# Provenance: erev0s/VAmPI get_by_title (fixed). +# repo: erev0s/VAmPI +# commit: 1713b54b601ad29582581eeda4b31fceb1319874 +# parent: 1713b54b601ad29582581eeda4b31fceb1319874 +# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51 +# cve: +# license: MIT +# function: get_by_title +# relpath: api_views/books.py +# provenance: human +# mechanism: missing_auth_guard + +def get_by_title(book_title): + resp = token_validator(request.headers.get('Authorization')) + if "error" in resp: + return Response(error_message_helper(resp), 401, mimetype="application/json") + else: + if vuln: # Broken Object Level Authorization + book = Book.query.filter_by(book_title=str(book_title)).first() + if book: + responseObject = { + 'book_title': book.book_title, + 'secret': book.secret_content, + 'owner': book.user.username + } + return Response(json.dumps(responseObject), 200, mimetype="application/json") + else: + return Response(error_message_helper("Book not found!"), 404, mimetype="application/json") + else: + user = User.query.filter_by(username=resp['sub']).first() + book = Book.query.filter_by(user=user, book_title=str(book_title)).first() + if book: + responseObject = { + 'book_title': book.book_title, + 'secret': book.secret_content, + 'owner': book.user.username + } + return Response(json.dumps(responseObject), 200, mimetype="application/json") + else: + return Response(error_message_helper("Book not found!"), 404, mimetype="application/json") diff --git a/benchmarks/pairs/vibe-py/vampi-books-get-by-title/vuln.py b/benchmarks/pairs/vibe-py/vampi-books-get-by-title/vuln.py new file mode 100644 index 0000000..5e24bb1 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vampi-books-get-by-title/vuln.py @@ -0,0 +1,40 @@ +# Provenance: erev0s/VAmPI get_by_title (vuln). +# repo: erev0s/VAmPI +# commit: 1713b54b601ad29582581eeda4b31fceb1319874 +# parent: 1713b54b601ad29582581eeda4b31fceb1319874 +# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/books.py#L51 +# cve: +# license: MIT +# function: get_by_title +# relpath: api_views/books.py +# provenance: human +# mechanism: missing_auth_guard + +def get_by_title(book_title): + resp = token_validator(request.headers.get('Authorization')) + if "error" in resp: + return Response(error_message_helper(resp), 401, mimetype="application/json") + else: + if vuln: # Broken Object Level Authorization + book = Book.query.filter_by(book_title=str(book_title)).first() + if book: + responseObject = { + 'book_title': book.book_title, + 'secret': book.secret_content, + 'owner': book.user.username + } + return Response(json.dumps(responseObject), 200, mimetype="application/json") + else: + return Response(error_message_helper("Book not found!"), 404, mimetype="application/json") + else: + user = User.query.filter_by(username=resp['sub']).first() + book = Book.query.filter_by(user=user, book_title=str(book_title)).first() + if book: + responseObject = { + 'book_title': book.book_title, + 'secret': book.secret_content, + 'owner': book.user.username + } + return Response(json.dumps(responseObject), 200, mimetype="application/json") + else: + return Response(error_message_helper("Book not found!"), 404, mimetype="application/json") diff --git a/benchmarks/pairs/vibe-py/vampi-user-model-get-user/fixed.py b/benchmarks/pairs/vibe-py/vampi-user-model-get-user/fixed.py new file mode 100644 index 0000000..9164899 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vampi-user-model-get-user/fixed.py @@ -0,0 +1,43 @@ +# Provenance: erev0s/VAmPI get_user (fixed). +# repo: erev0s/VAmPI +# commit: 1713b54b601ad29582581eeda4b31fceb1319874 +# parent: 1713b54b601ad29582581eeda4b31fceb1319874 +# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/models/user_model.py#L72 +# cve: +# license: MIT +# function: get_user +# relpath: models/user_model.py +# provenance: human +# mechanism: source_reaches_sink + +def register_user(): + request_data = request.get_json() + # check if user already exists + user = User.query.filter_by(username=request_data.get('username')).first() + if not user: + try: + # validate the data are in the correct form + jsonschema.validate(request_data, register_user_schema) + if vuln and 'admin' in request_data: # User is possible to define if she/he wants to be an admin !! + if request_data['admin']: + admin = True + else: + admin = False + user = User(username=request_data['username'], password=request_data['password'], + email=request_data['email'], admin=admin) + else: + user = User(username=request_data['username'], password=request_data['password'], + email=request_data['email']) + db.session.add(user) + db.session.commit() + + responseObject = { + 'status': 'success', + 'message': 'Successfully registered. Login to receive an auth token.' + } + + return Response(json.dumps(responseObject), 200, mimetype="application/json") + except jsonschema.exceptions.ValidationError as exc: + return Response(error_message_helper(exc.message), 400, mimetype="application/json") + else: + return Response(error_message_helper("User already exists. Please Log in."), 200, mimetype="application/json") diff --git a/benchmarks/pairs/vibe-py/vampi-user-model-get-user/vuln.py b/benchmarks/pairs/vibe-py/vampi-user-model-get-user/vuln.py new file mode 100644 index 0000000..2d9ffcb --- /dev/null +++ b/benchmarks/pairs/vibe-py/vampi-user-model-get-user/vuln.py @@ -0,0 +1,24 @@ +# Provenance: erev0s/VAmPI get_user (vuln). +# repo: erev0s/VAmPI +# commit: 1713b54b601ad29582581eeda4b31fceb1319874 +# parent: 1713b54b601ad29582581eeda4b31fceb1319874 +# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/models/user_model.py#L72 +# cve: +# license: MIT +# function: get_user +# relpath: models/user_model.py +# provenance: human +# mechanism: source_reaches_sink + + def get_user(username): + if vuln: # SQLi Injection + user_query = f"SELECT * FROM users WHERE username = '{username}'" + query = db.session.execute(text(user_query)) + ret = query.fetchone() + if ret: + fin_query = '{"username": "%s", "email": "%s"}' % (ret[1], ret[3]) + else: + fin_query = None + else: + fin_query = User.query.filter_by(username=username).first() + return fin_query diff --git a/benchmarks/pairs/vibe-py/vampi-users-update-password/fixed.py b/benchmarks/pairs/vibe-py/vampi-users-update-password/fixed.py new file mode 100644 index 0000000..038c9b3 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vampi-users-update-password/fixed.py @@ -0,0 +1,43 @@ +# Provenance: erev0s/VAmPI update_password (fixed). +# repo: erev0s/VAmPI +# commit: 1713b54b601ad29582581eeda4b31fceb1319874 +# parent: 1713b54b601ad29582581eeda4b31fceb1319874 +# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187 +# cve: +# license: MIT +# function: update_password +# relpath: api_views/users.py +# provenance: human +# mechanism: missing_auth_guard + +def register_user(): + request_data = request.get_json() + # check if user already exists + user = User.query.filter_by(username=request_data.get('username')).first() + if not user: + try: + # validate the data are in the correct form + jsonschema.validate(request_data, register_user_schema) + if vuln and 'admin' in request_data: # User is possible to define if she/he wants to be an admin !! + if request_data['admin']: + admin = True + else: + admin = False + user = User(username=request_data['username'], password=request_data['password'], + email=request_data['email'], admin=admin) + else: + user = User(username=request_data['username'], password=request_data['password'], + email=request_data['email']) + db.session.add(user) + db.session.commit() + + responseObject = { + 'status': 'success', + 'message': 'Successfully registered. Login to receive an auth token.' + } + + return Response(json.dumps(responseObject), 200, mimetype="application/json") + except jsonschema.exceptions.ValidationError as exc: + return Response(error_message_helper(exc.message), 400, mimetype="application/json") + else: + return Response(error_message_helper("User already exists. Please Log in."), 200, mimetype="application/json") diff --git a/benchmarks/pairs/vibe-py/vampi-users-update-password/vuln.py b/benchmarks/pairs/vibe-py/vampi-users-update-password/vuln.py new file mode 100644 index 0000000..2cd7733 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vampi-users-update-password/vuln.py @@ -0,0 +1,37 @@ +# Provenance: erev0s/VAmPI update_password (vuln). +# repo: erev0s/VAmPI +# commit: 1713b54b601ad29582581eeda4b31fceb1319874 +# parent: 1713b54b601ad29582581eeda4b31fceb1319874 +# commit_url: https://github.com/erev0s/VAmPI/blob/1713b54b601ad29582581eeda4b31fceb1319874/api_views/users.py#L187 +# cve: +# license: MIT +# function: update_password +# relpath: api_views/users.py +# provenance: human +# mechanism: missing_auth_guard + +def update_password(username): + request_data = request.get_json() + resp = token_validator(request.headers.get('Authorization')) + if "error" in resp: + return Response(error_message_helper(resp), 401, mimetype="application/json") + else: + if request_data.get('password'): + if vuln: # Unauthorized update of password of another user + user = User.query.filter_by(username=username).first() + if user: + user.password = request_data.get('password') + db.session.commit() + else: + return Response(error_message_helper("User Not Found"), 400, mimetype="application/json") + else: + user = User.query.filter_by(username=resp['sub']).first() + user.password = request_data.get('password') + db.session.commit() + responseObject = { + 'status': 'success', + 'Password': 'Updated.' + } + return Response(json.dumps(responseObject), 204, mimetype="application/json") + else: + return Response(error_message_helper("Malformed Data"), 400, mimetype="application/json") diff --git a/benchmarks/pairs/vibe-py/vfapi-main-delete-user/fixed.py b/benchmarks/pairs/vibe-py/vfapi-main-delete-user/fixed.py new file mode 100644 index 0000000..49d1d1b --- /dev/null +++ b/benchmarks/pairs/vibe-py/vfapi-main-delete-user/fixed.py @@ -0,0 +1,14 @@ +# Provenance: naryal2580/vfapi delete_user (fixed). +# repo: naryal2580/vfapi +# commit: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# parent: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# commit_url: https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L231 +# cve: +# license: MIT +# function: delete_user +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink + +async def nosql_return_users_from_username(username: str): + return get_nosql_users({'username': username}) diff --git a/benchmarks/pairs/vibe-py/vfapi-main-delete-user/vuln.py b/benchmarks/pairs/vibe-py/vfapi-main-delete-user/vuln.py new file mode 100644 index 0000000..175e8b7 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vfapi-main-delete-user/vuln.py @@ -0,0 +1,21 @@ +# Provenance: naryal2580/vfapi delete_user (vuln). +# repo: naryal2580/vfapi +# commit: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# parent: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# commit_url: https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L231 +# cve: +# license: MIT +# function: delete_user +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink + +async def delete_user(username: Optional[str] = '', user: Optional[User] = None): + if username: + db_client.vfapi.users.delete_one({'username': username}) + await run_sql_query(f'DELETE FROM users WHERE username = "{username}";', commit=True) + return {'resp': 'done'} + elif user: + db_client.vfapi.users.delete_one({'address': user.address}) + await run_sql_query(f'DELETE FROM users WHERE address = {user.address};', commit=True) + return {'resp': '!done'} diff --git a/benchmarks/pairs/vibe-py/vfapi-main-put-user/fixed.py b/benchmarks/pairs/vibe-py/vfapi-main-put-user/fixed.py new file mode 100644 index 0000000..f72cbfb --- /dev/null +++ b/benchmarks/pairs/vibe-py/vfapi-main-put-user/fixed.py @@ -0,0 +1,15 @@ +# Provenance: naryal2580/vfapi put_user (fixed). +# repo: naryal2580/vfapi +# commit: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# parent: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# commit_url: https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L188 +# cve: +# license: MIT +# function: put_user +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink +# upstream_start: 219 + +async def nosql_return_users_from_username(username: str): + return get_nosql_users({'username': username}) diff --git a/benchmarks/pairs/vibe-py/vfapi-main-put-user/vuln.py b/benchmarks/pairs/vibe-py/vfapi-main-put-user/vuln.py new file mode 100644 index 0000000..5d53ee3 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vfapi-main-put-user/vuln.py @@ -0,0 +1,44 @@ +# Provenance: naryal2580/vfapi put_user (vuln). +# repo: naryal2580/vfapi +# commit: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# parent: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# commit_url: https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L188 +# cve: +# license: MIT +# function: put_user +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink +# upstream_start: 186 + +async def put_user(user: User): + user.password = md5(user.password.encode()).hexdigest() + query = f''' +INSERT INTO users ( + name, + username, + password, + address, + email, + contact + ) VALUES ( + "{user.name}", + "{user.username}", + "{user.password}", + "{user.address}", + "{user.email}", + "{user.contact}" + ); +'''[1:-1] + await run_sql_query(query, commit=True) + _id = await run_sql_query('SELECT id from users ORDER BY ROWID DESC limit 1;') + db_client.vfapi.users.insert_one({ + 'id': _id, + 'name': user.name, + 'username': user.username, + 'password': user.password, + 'address': user.address, + 'email': user.email, + 'contact': user.contact + }) + return {'resp': 'done'} diff --git a/benchmarks/pairs/vibe-py/vfapi-main-sql-return-users-from-username/fixed.py b/benchmarks/pairs/vibe-py/vfapi-main-sql-return-users-from-username/fixed.py new file mode 100644 index 0000000..e3b6345 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vfapi-main-sql-return-users-from-username/fixed.py @@ -0,0 +1,14 @@ +# Provenance: naryal2580/vfapi sql_return_users_from_username (fixed). +# repo: naryal2580/vfapi +# commit: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# parent: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# commit_url: https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L182 +# cve: +# license: MIT +# function: sql_return_users_from_username +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink + +async def nosql_return_users_from_username(username: str): + return get_nosql_users({'username': username}) diff --git a/benchmarks/pairs/vibe-py/vfapi-main-sql-return-users-from-username/vuln.py b/benchmarks/pairs/vibe-py/vfapi-main-sql-return-users-from-username/vuln.py new file mode 100644 index 0000000..d0891e5 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vfapi-main-sql-return-users-from-username/vuln.py @@ -0,0 +1,15 @@ +# Provenance: naryal2580/vfapi sql_return_users_from_username (vuln). +# repo: naryal2580/vfapi +# commit: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# parent: f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3 +# commit_url: https://github.com/naryal2580/vfapi/blob/f36f177e1a32aa49272f4eb52f8be0af3e8f6bc3/main.py#L182 +# cve: +# license: MIT +# function: sql_return_users_from_username +# relpath: main.py +# provenance: human +# mechanism: source_reaches_sink + +async def sql_return_users_from_username(username: str): + resp = await run_sql_query(f'SELECT * FROM users WHERE username = "{username}";') + return resp diff --git a/benchmarks/pairs/vibe-py/vulnerable-python-apps-bad-mvc-do-get/fixed.py b/benchmarks/pairs/vibe-py/vulnerable-python-apps-bad-mvc-do-get/fixed.py new file mode 100644 index 0000000..9c8b83d --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnerable-python-apps-bad-mvc-do-get/fixed.py @@ -0,0 +1,20 @@ +# Provenance: LinuxUser255/Vulnerable_Python_Apps do_GET (fixed). +# repo: LinuxUser255/Vulnerable_Python_Apps +# commit: 6336c77e6db523a5df34c4b628dff4159110797a +# parent: 6336c77e6db523a5df34c4b628dff4159110797a +# commit_url: https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Insecure_Frameworks/bad_mvc.py#L37 +# cve: +# license: GPL-3.0 +# function: do_GET +# relpath: Insecure_Frameworks/bad_mvc.py +# provenance: human +# mechanism: source_reaches_sink + +def get_post(post_id): + conn = get_db_connection() # SQL configured to schema.sql + post = conn.execute('SELECT * FROM posts WHERE id = ?', + (post_id,)).fetchone() + conn.close() + if post is None: + abort(404) + return post diff --git a/benchmarks/pairs/vibe-py/vulnerable-python-apps-bad-mvc-do-get/vuln.py b/benchmarks/pairs/vibe-py/vulnerable-python-apps-bad-mvc-do-get/vuln.py new file mode 100644 index 0000000..91008bf --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnerable-python-apps-bad-mvc-do-get/vuln.py @@ -0,0 +1,29 @@ +# Provenance: LinuxUser255/Vulnerable_Python_Apps do_GET (vuln). +# repo: LinuxUser255/Vulnerable_Python_Apps +# commit: 6336c77e6db523a5df34c4b628dff4159110797a +# parent: 6336c77e6db523a5df34c4b628dff4159110797a +# commit_url: https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Insecure_Frameworks/bad_mvc.py#L37 +# cve: +# license: GPL-3.0 +# function: do_GET +# relpath: Insecure_Frameworks/bad_mvc.py +# provenance: human +# mechanism: source_reaches_sink + + def do_GET(self): + cookies = SimpleCookie(self.headers.get('Cookie')) + if cookies.get('session_id'): + try: + username = self.decrypt(cookies.get('session_id').value) # regex and escaped all inputs + except: + self.send_response(500) + return + else: + username = 'Anonymous' + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(bytes("Hello", "utf-8")) + self.wfile.write(bytes("", "utf-8")) + self.wfile.write(bytes("Hello %s" % username, "utf-8")) # needs encoding + self.wfile.write(bytes("", "utf-8")) diff --git a/benchmarks/pairs/vibe-py/vulnerable-python-apps-dir-traversal-do-get/fixed.py b/benchmarks/pairs/vibe-py/vulnerable-python-apps-dir-traversal-do-get/fixed.py new file mode 100644 index 0000000..3c3853a --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnerable-python-apps-dir-traversal-do-get/fixed.py @@ -0,0 +1,20 @@ +# Provenance: LinuxUser255/Vulnerable_Python_Apps do_GET (fixed). +# repo: LinuxUser255/Vulnerable_Python_Apps +# commit: 6336c77e6db523a5df34c4b628dff4159110797a +# parent: 6336c77e6db523a5df34c4b628dff4159110797a +# commit_url: https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Directory_Traversal/Dir_Traversal.py#L22 +# cve: +# license: GPL-3.0 +# function: do_GET +# relpath: Directory_Traversal/Dir_Traversal.py +# provenance: human +# mechanism: path_join_user_input + +def get_post(post_id): + conn = get_db_connection() # SQL configured to schema.sql + post = conn.execute('SELECT * FROM posts WHERE id = ?', + (post_id,)).fetchone() + conn.close() + if post is None: + abort(404) + return post diff --git a/benchmarks/pairs/vibe-py/vulnerable-python-apps-dir-traversal-do-get/vuln.py b/benchmarks/pairs/vibe-py/vulnerable-python-apps-dir-traversal-do-get/vuln.py new file mode 100644 index 0000000..cedd734 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnerable-python-apps-dir-traversal-do-get/vuln.py @@ -0,0 +1,29 @@ +# Provenance: LinuxUser255/Vulnerable_Python_Apps do_GET (vuln). +# repo: LinuxUser255/Vulnerable_Python_Apps +# commit: 6336c77e6db523a5df34c4b628dff4159110797a +# parent: 6336c77e6db523a5df34c4b628dff4159110797a +# commit_url: https://github.com/LinuxUser255/Vulnerable_Python_Apps/blob/6336c77e6db523a5df34c4b628dff4159110797a/Directory_Traversal/Dir_Traversal.py#L22 +# cve: +# license: GPL-3.0 +# function: do_GET +# relpath: Directory_Traversal/Dir_Traversal.py +# provenance: human +# mechanism: path_join_user_input + + def do_GET(self): # no data type receive defined + path = os.getcwd() # dir traversal and possible LFI + pattern = r'/\.\.\/\.\.\/' # weak regex pattern + if re.match(pattern, self.path): # escape characters needed + self.send_response(404) + return + path += self.path + if path.endswith('/'): + path += 'index.html' + print(path) + if exists(path): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(open(path).read().encode('utf-8')) # path is open no string mod + else: + self.send_response(404) diff --git a/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-os-system/fixed.py b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-os-system/fixed.py new file mode 100644 index 0000000..9e5c8da --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-os-system/fixed.py @@ -0,0 +1,21 @@ +# Provenance: Contrast-Security-OSS/vulnpy do_os_system (fixed). +# repo: Contrast-Security-OSS/vulnpy +# commit: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# parent: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# commit_url: https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L6 +# cve: +# license: MIT +# function: do_os_system +# relpath: src/vulnpy/trigger/cmdi.py +# provenance: human +# mechanism: source_reaches_sink + +def _execute(db_func): + try: + db_connection = _db_reset() + cursor = db_connection.cursor() + db_func(cursor) + all_rows = cursor.execute(SELECT_ALL) + return ";".join([",".join(row) for row in all_rows]) + except Exception: + return "error" diff --git a/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-os-system/vuln.py b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-os-system/vuln.py new file mode 100644 index 0000000..a053551 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-os-system/vuln.py @@ -0,0 +1,14 @@ +# Provenance: Contrast-Security-OSS/vulnpy do_os_system (vuln). +# repo: Contrast-Security-OSS/vulnpy +# commit: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# parent: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# commit_url: https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L6 +# cve: +# license: MIT +# function: do_os_system +# relpath: src/vulnpy/trigger/cmdi.py +# provenance: human +# mechanism: source_reaches_sink + +def do_os_system(command): + return os.system(command) diff --git a/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-subprocess-popen/fixed.py b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-subprocess-popen/fixed.py new file mode 100644 index 0000000..45cea1f --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-subprocess-popen/fixed.py @@ -0,0 +1,21 @@ +# Provenance: Contrast-Security-OSS/vulnpy do_subprocess_popen (fixed). +# repo: Contrast-Security-OSS/vulnpy +# commit: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# parent: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# commit_url: https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L10 +# cve: +# license: MIT +# function: do_subprocess_popen +# relpath: src/vulnpy/trigger/cmdi.py +# provenance: human +# mechanism: source_reaches_sink + +def _execute(db_func): + try: + db_connection = _db_reset() + cursor = db_connection.cursor() + db_func(cursor) + all_rows = cursor.execute(SELECT_ALL) + return ";".join([",".join(row) for row in all_rows]) + except Exception: + return "error" diff --git a/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-subprocess-popen/vuln.py b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-subprocess-popen/vuln.py new file mode 100644 index 0000000..eda17b6 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnpy-cmdi-do-subprocess-popen/vuln.py @@ -0,0 +1,18 @@ +# Provenance: Contrast-Security-OSS/vulnpy do_subprocess_popen (vuln). +# repo: Contrast-Security-OSS/vulnpy +# commit: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# parent: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# commit_url: https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/cmdi.py#L10 +# cve: +# license: MIT +# function: do_subprocess_popen +# relpath: src/vulnpy/trigger/cmdi.py +# provenance: human +# mechanism: source_reaches_sink + +def do_subprocess_popen(command): + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True + ) + stdout, _ = process.communicate() + return stdout diff --git a/benchmarks/pairs/vibe-py/vulnpy-deserialization-do-pickle-load/fixed.py b/benchmarks/pairs/vibe-py/vulnpy-deserialization-do-pickle-load/fixed.py new file mode 100644 index 0000000..4779380 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnpy-deserialization-do-pickle-load/fixed.py @@ -0,0 +1,15 @@ +# Provenance: Contrast-Security-OSS/vulnpy do_pickle_load (fixed). +# repo: Contrast-Security-OSS/vulnpy +# commit: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# parent: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# commit_url: https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/deserialization.py#L8 +# cve: +# license: MIT +# function: do_pickle_load +# relpath: src/vulnpy/trigger/deserialization.py +# provenance: human +# mechanism: source_reaches_sink + +def do_pickle_load(user_input): + user_input = io.BytesIO(user_input.encode("utf-8")) + return pickle.load(user_input) diff --git a/benchmarks/pairs/vibe-py/vulnpy-deserialization-do-pickle-load/vuln.py b/benchmarks/pairs/vibe-py/vulnpy-deserialization-do-pickle-load/vuln.py new file mode 100644 index 0000000..0983413 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulnpy-deserialization-do-pickle-load/vuln.py @@ -0,0 +1,15 @@ +# Provenance: Contrast-Security-OSS/vulnpy do_pickle_load (vuln). +# repo: Contrast-Security-OSS/vulnpy +# commit: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# parent: d1d0f64c06f038fe59a049e0e6001bfaf9839697 +# commit_url: https://github.com/Contrast-Security-OSS/vulnpy/blob/d1d0f64c06f038fe59a049e0e6001bfaf9839697/src/vulnpy/trigger/deserialization.py#L8 +# cve: +# license: MIT +# function: do_pickle_load +# relpath: src/vulnpy/trigger/deserialization.py +# provenance: human +# mechanism: source_reaches_sink + +def do_pickle_load(user_input): + user_input = io.BytesIO(user_input.encode("utf-8")) + return pickle.load(user_input) diff --git a/benchmarks/pairs/vibe-py/vulpy-libuser-create/fixed.py b/benchmarks/pairs/vibe-py/vulpy-libuser-create/fixed.py new file mode 100644 index 0000000..df53f3f --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulpy-libuser-create/fixed.py @@ -0,0 +1,24 @@ +# Provenance: fportantier/vulpy create (fixed). +# repo: fportantier/vulpy +# commit: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# parent: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# commit_url: https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L25 +# cve: +# license: MIT +# function: create +# relpath: bad/libuser.py +# provenance: human +# mechanism: source_reaches_sink + +def get_posts(username): + + conn = sqlite3.connect('db_posts.sqlite') + conn.set_trace_callback(print) + conn.row_factory = sqlite3.Row + c = conn.cursor() + + rows = c.execute("SELECT * FROM posts WHERE username = ? ORDER BY date DESC", (username,)).fetchall() + + posts = [ dict(zip(row.keys(), row)) for row in rows ] + + return posts diff --git a/benchmarks/pairs/vibe-py/vulpy-libuser-create/vuln.py b/benchmarks/pairs/vibe-py/vulpy-libuser-create/vuln.py new file mode 100644 index 0000000..1a17a73 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulpy-libuser-create/vuln.py @@ -0,0 +1,21 @@ +# Provenance: fportantier/vulpy create (vuln). +# repo: fportantier/vulpy +# commit: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# parent: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# commit_url: https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L25 +# cve: +# license: MIT +# function: create +# relpath: bad/libuser.py +# provenance: human +# mechanism: source_reaches_sink + +def create(username, password): + + conn = sqlite3.connect('db_users.sqlite') + c = conn.cursor() + + c.execute("INSERT INTO users (username, password, failures, mfa_enabled, mfa_secret) VALUES ('%s', '%s', '%d', '%d', '%s')" %(username, password, 0, 0, '')) + + conn.commit() + conn.close() diff --git a/benchmarks/pairs/vibe-py/vulpy-libuser-login/fixed.py b/benchmarks/pairs/vibe-py/vulpy-libuser-login/fixed.py new file mode 100644 index 0000000..3047b3a --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulpy-libuser-login/fixed.py @@ -0,0 +1,24 @@ +# Provenance: fportantier/vulpy login (fixed). +# repo: fportantier/vulpy +# commit: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# parent: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# commit_url: https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L12 +# cve: +# license: MIT +# function: login +# relpath: bad/libuser.py +# provenance: human +# mechanism: source_reaches_sink + +def get_posts(username): + + conn = sqlite3.connect('db_posts.sqlite') + conn.set_trace_callback(print) + conn.row_factory = sqlite3.Row + c = conn.cursor() + + rows = c.execute("SELECT * FROM posts WHERE username = ? ORDER BY date DESC", (username,)).fetchall() + + posts = [ dict(zip(row.keys(), row)) for row in rows ] + + return posts diff --git a/benchmarks/pairs/vibe-py/vulpy-libuser-login/vuln.py b/benchmarks/pairs/vibe-py/vulpy-libuser-login/vuln.py new file mode 100644 index 0000000..560ae46 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulpy-libuser-login/vuln.py @@ -0,0 +1,25 @@ +# Provenance: fportantier/vulpy login (vuln). +# repo: fportantier/vulpy +# commit: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# parent: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# commit_url: https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L12 +# cve: +# license: MIT +# function: login +# relpath: bad/libuser.py +# provenance: human +# mechanism: source_reaches_sink + +def login(username, password): + + conn = sqlite3.connect('db_users.sqlite') + conn.set_trace_callback(print) + conn.row_factory = sqlite3.Row + c = conn.cursor() + + user = c.execute("SELECT * FROM users WHERE username = '{}' and password = '{}'".format(username, password)).fetchone() + + if user: + return user['username'] + else: + return False diff --git a/benchmarks/pairs/vibe-py/vulpy-libuser-password-change/fixed.py b/benchmarks/pairs/vibe-py/vulpy-libuser-password-change/fixed.py new file mode 100644 index 0000000..1fa28c9 --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulpy-libuser-password-change/fixed.py @@ -0,0 +1,24 @@ +# Provenance: fportantier/vulpy password_change (fixed). +# repo: fportantier/vulpy +# commit: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# parent: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# commit_url: https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L53 +# cve: +# license: MIT +# function: password_change +# relpath: bad/libuser.py +# provenance: human +# mechanism: source_reaches_sink + +def get_posts(username): + + conn = sqlite3.connect('db_posts.sqlite') + conn.set_trace_callback(print) + conn.row_factory = sqlite3.Row + c = conn.cursor() + + rows = c.execute("SELECT * FROM posts WHERE username = ? ORDER BY date DESC", (username,)).fetchall() + + posts = [ dict(zip(row.keys(), row)) for row in rows ] + + return posts diff --git a/benchmarks/pairs/vibe-py/vulpy-libuser-password-change/vuln.py b/benchmarks/pairs/vibe-py/vulpy-libuser-password-change/vuln.py new file mode 100644 index 0000000..a743bca --- /dev/null +++ b/benchmarks/pairs/vibe-py/vulpy-libuser-password-change/vuln.py @@ -0,0 +1,23 @@ +# Provenance: fportantier/vulpy password_change (vuln). +# repo: fportantier/vulpy +# commit: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# parent: 5249cc8b05a1c37f6b2f757b1cf16a509c327122 +# commit_url: https://github.com/fportantier/vulpy/blob/5249cc8b05a1c37f6b2f757b1cf16a509c327122/bad/libuser.py#L53 +# cve: +# license: MIT +# function: password_change +# relpath: bad/libuser.py +# provenance: human +# mechanism: source_reaches_sink + +def password_change(username, password): + + conn = sqlite3.connect('db_users.sqlite') + conn.set_trace_callback(print) + conn.row_factory = sqlite3.Row + c = conn.cursor() + + c.execute("UPDATE users SET password = '{}' WHERE username = '{}'".format(password, username)) + conn.commit() + + return True