From 245c71873f6474c003d992fc22cfee65a3de7b3e Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 00:17:04 +0900 Subject: [PATCH 01/10] feat(scan): filter cdxgen SBOMs to the runtime scope before persist and matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cdxgen keeps every resolved node, so a deployed app's SBOM also carried its test/provided/dev toolchain (junit, lombok, devDependencies) as if it shipped — inflating CVE counts, license obligations and NOTICE content. Mirror the BomLens build-prep post-filters (#331/#335/#341) as a worker-side stage: - integrations/sbom_scope_filter.py: pure post-filter. Maven drops cdxgen scope optional/excluded behind a hasScopes guard (an unscoped SBOM is left whole so recall never regresses). Node diverges from BomLens deliberately: instead of an npm re-resolve subprocess, drop pkg:npm components the already-parsed package-lock.json positively classifies dev (hasDev guard, keep-if-unknown for purls the root lockfile does not cover). Shared tail prunes dependencies[] to kept refs and always preserves the root ref. - Pipeline wiring runs copy-then-commit BEFORE artifact persist, cosign signing, SCANOSS and Trivy so every consumer sees one consistent filtered document; a failed disk rewrite degrades to filter-skipped with memory and disk still identical. Telemetry lands in scan_metadata.scope_filter and a trusca:scope_filter SBOM property; no new pipeline stage (sub-second step). - Ingest path deliberately unfiltered: an uploaded SBOM is the supplier's declared truth (pinned by an AST regression test). - Config: SCAN_SCOPE_FILTER_ENABLED / _MAVEN_ENABLED / _NODE_ENABLED, all default ON; only exact falsy tokens disable, so a typo fails open to the correct-by-default behaviour (inverse of SCANOSS_ENABLED, no egress here). - Fixtures are real captured cdxgen 12.3.3 output (Spring Boot 3.2 Maven app: 108 components with required/optional/excluded; Express app: 487 components + its 502-package lockfile, 377 dev-flagged). - Docs EN/KO: runtime-scope filtering section, env vars, stage description; also refresh the stale WebSocket slug list (dt_upload/dt_findings era). --- CHANGELOG.md | 13 + apps/backend/core/config.py | 53 + .../backend/integrations/sbom_scope_filter.py | 292 + apps/backend/tasks/ingest_sbom.py | 9 + apps/backend/tasks/scan_source.py | 91 +- .../fixtures/npm/package-lock.devdeps.json | 5526 +++++++++++++++++ .../sbom/real_cyclonedx_maven_scoped.json | 1 + .../sbom/real_cyclonedx_node_dev.json | 1 + .../integrations/test_sbom_scope_filter.py | 330 + .../tasks/test_scan_source_scope_filter.py | 350 ++ .../tests/unit/test_scope_filter_config.py | 66 + docs-site/docs/reference/env-variables.md | 3 + .../user-guide/components-and-licenses.md | 32 + docs-site/docs/user-guide/scans.md | 8 +- .../current/reference/env-variables.md | 3 + .../user-guide/components-and-licenses.md | 31 + .../current/user-guide/scans.md | 8 +- 17 files changed, 6808 insertions(+), 9 deletions(-) create mode 100644 apps/backend/integrations/sbom_scope_filter.py create mode 100644 apps/backend/tests/fixtures/npm/package-lock.devdeps.json create mode 100644 apps/backend/tests/fixtures/sbom/real_cyclonedx_maven_scoped.json create mode 100644 apps/backend/tests/fixtures/sbom/real_cyclonedx_node_dev.json create mode 100644 apps/backend/tests/unit/integrations/test_sbom_scope_filter.py create mode 100644 apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py create mode 100644 apps/backend/tests/unit/test_scope_filter_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f33ba7e4..fd15e179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] +### Added +- **Runtime-scope SBOM filtering (default ON).** Source scans now drop + non-deployable dependencies from the cdxgen SBOM before persist, signing and + vulnerability matching: Maven `test`/`provided` nodes (cdxgen scope tags + `optional`/`excluded`) and npm `devDependencies` (lockfile-classified `dev`). + CVE counts and license obligations now describe the artifact that actually + ships. **Component and CVE counts drop on the first re-scan of affected + Maven/npm projects** — the scan summary records how many components were + excluded, and `SCAN_SCOPE_FILTER_ENABLED=false` (or the per-ecosystem + `SCAN_SCOPE_FILTER_MAVEN_ENABLED` / `SCAN_SCOPE_FILTER_NODE_ENABLED`) + restores the full graph. SBOMs uploaded via the ingest API are never + filtered — an uploaded SBOM is the supplier's declared truth. + ## [0.13.1] — 2026-07-07 A fixes-only patch release: repairs fresh role-separated (L1) installs and diff --git a/apps/backend/core/config.py b/apps/backend/core/config.py index d166daf7..73373beb 100644 --- a/apps/backend/core/config.py +++ b/apps/backend/core/config.py @@ -505,6 +505,59 @@ def cdxgen_fetch_license() -> bool: } +# --------------------------------------------------------------------------- +# Runtime-scope SBOM post-filter (Phase K — default ON). +# +# Drops test/provided/dev dependencies from the cdxgen SBOM before persist, +# signing and Trivy matching, so CVE counts and license obligations describe +# the deployable artifact (BomLens build-prep parity). Unlike SCANOSS this is +# a pure local transformation with no egress, so it defaults ON; the guards +# inside integrations/sbom_scope_filter.py make it a structural no-op wherever +# the data to filter safely is absent. Parsing is inverted vs scanoss_enabled: +# only the exact falsy tokens disable, so a typo fails OPEN to the +# correct-by-default behaviour rather than silently reverting to over-counting. +# --------------------------------------------------------------------------- + + +_SCOPE_FILTER_FALSY = {"false", "0", "no"} + + +def scan_scope_filter_enabled() -> bool: + """Master switch for the runtime-scope post-filter stage. + + Default ``true``. Only ``false`` / ``0`` / ``no`` (case-insensitive) + disable it. Read at call time (rule #11). + """ + return ( + os.getenv("SCAN_SCOPE_FILTER_ENABLED", "true").strip().lower() + not in _SCOPE_FILTER_FALSY + ) + + +def scan_scope_filter_maven_enabled() -> bool: + """Maven scope-tag filter (drop ``optional``/``excluded`` maven nodes). + + Default ``true``; disable when a project relies on Maven + ``true`` runtime deps (cdxgen tags those ``optional`` + like test scope — documented caveat). Read at call time (rule #11). + """ + return ( + os.getenv("SCAN_SCOPE_FILTER_MAVEN_ENABLED", "true").strip().lower() + not in _SCOPE_FILTER_FALSY + ) + + +def scan_scope_filter_node_enabled() -> bool: + """npm dev-dependency filter (drop lockfile-classified ``dev`` nodes). + + Default ``true``. Read at call time (rule #11). + """ + return ( + os.getenv("SCAN_SCOPE_FILTER_NODE_ENABLED", "true").strip().lower() + not in _SCOPE_FILTER_FALSY + ) + + def scan_executor_mode() -> str: """How the SBOM-generation stage (build-prep + cdxgen) is executed. diff --git a/apps/backend/integrations/sbom_scope_filter.py b/apps/backend/integrations/sbom_scope_filter.py new file mode 100644 index 00000000..67e22e39 --- /dev/null +++ b/apps/backend/integrations/sbom_scope_filter.py @@ -0,0 +1,292 @@ +""" +Runtime-scope SBOM post-filter — drop non-shipping dependencies (Phase K). + +cdxgen keeps every *resolved* node, so a deployed application's SBOM also +carries its test / provided / dev toolchain (junit, lombok, devDependencies) +as if those shipped. Downstream that inflates CVE counts, license obligations +and NOTICE content for artifacts that never reach production. This module +filters the CycloneDX document down to the deployable runtime set, mirroring +BomLens ``build-prep.sh`` (#331/#335/#337/#341): + + * **Maven** — data-driven off cdxgen's own scope tags. cdxgen maps Maven + ``compile``/``runtime`` → ``required`` (kept), ``test`` → ``optional`` + (dropped), ``provided``/``system`` → ``excluded`` (dropped). Guard: the + filter only runs when at least one ``pkg:maven/`` component carries + ``scope == "required"`` (the *hasScopes* guard) — an SBOM whose producer + populated no scopes (e.g. a fallback generator) is left untouched so + recall never regresses. Known caveat (documented in the user guide): + cdxgen also tags ``true`` runtime deps as + ``optional``, so those are dropped too; ``SCAN_SCOPE_FILTER_MAVEN_ENABLED=false`` + is the escape hatch. + * **Node** — deliberate divergence from BomLens (which re-resolves the + production set via ``npm install --omit=dev``): TRUSCA's prep stage + already guarantees a ``package-lock.json`` (``_prepare_npm``) and + :mod:`integrations.npm_lockfile` already classifies every installed + package as ``required``/``dev``/``optional``/``peer``. We drop a + ``pkg:npm/`` component only when the lockfile positively classifies it + ``dev``. Guards: the lockfile parsed (*hasLock*) AND classified at least + one entry ``dev`` (*hasDev*); an npm purl **absent** from the lockfile is + KEPT (keep-if-unknown — a monorepo's nested non-workspace manifests are + not covered by the root lockfile, and the filter must only remove + components it has positive dev evidence for). + +Shared tail (all ecosystems): kept refs = ``bom-ref``∥``purl`` of every kept +component **plus the ``metadata.component`` root ref**, then the +``dependencies[]`` graph is pruned to kept refs (entries dropped, each +``dependsOn`` filtered). + +Failure posture (BomLens parity): the filter NEVER raises and NEVER breaks a +scan — any error returns ``applied=False`` and the SBOM is left exactly as +cdxgen wrote it. Callers should filter a working copy and only commit it once +the on-disk rewrite succeeded, so the in-memory document, the persisted rows, +the signed artifact and Trivy's input can never diverge. + +Layering: pure functions only — no DB, no Celery, no config reads (the caller +resolves the ``SCAN_SCOPE_FILTER_*`` toggles). Same discipline as +:mod:`integrations.npm_lockfile`. +""" + +from __future__ import annotations + +import json +import os +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import structlog + +from integrations.npm_lockfile import NpmLockfileData + +log = structlog.get_logger("integrations.sbom_scope_filter") + +# Maven scope tags cdxgen emits for non-deployable nodes. ``required`` and +# *unscoped* components are kept (an unscoped node carries no evidence it is +# test-only — keep-if-unknown, same philosophy as the Node predicate). +_MAVEN_DROP_SCOPES = frozenset({"optional", "excluded"}) + +# SBOM metadata property stamped onto a filtered document so the signed / +# downloadable artifact self-documents what was removed (mirrors the +# ``bomlens:*`` provenance convention). +FILTER_PROPERTY_NAME = "trusca:scope_filter" + + +@dataclass(frozen=True) +class ScopeFilterResult: + """Outcome of one filter pass. + + Attributes: + applied: ``True`` when at least one ecosystem predicate was active + (its guards passed), even if it dropped nothing. ``False`` means + the document was left untouched — guards no-opped or an error + occurred. + dropped: per-ecosystem drop counts, only non-zero keys + (``{"maven": 12, "npm": 340}``). + kept_components: component count after filtering (unchanged count + when ``applied`` is ``False``). + """ + + applied: bool + dropped: dict[str, int] = field(default_factory=dict) + kept_components: int = 0 + + @property + def total_dropped(self) -> int: + return sum(self.dropped.values()) + + +def filter_sbom_to_runtime_scope( + sbom: dict[str, Any], + *, + npm_lock: NpmLockfileData | None, + maven: bool = True, + node: bool = True, +) -> ScopeFilterResult: + """Filter ``sbom`` (in place) down to the deployable runtime set. + + ``maven`` / ``node`` are the per-ecosystem toggles (resolved from config + by the caller). Never raises — on any error the document is left in its + pre-call state only if the error happened before the first mutation; + callers that need transactional semantics must pass a working copy (see + module docstring). + """ + try: + return _filter(sbom, npm_lock=npm_lock, maven=maven, node=node) + except Exception: # noqa: BLE001 — the filter must never break a scan + log.warning("scope_filter_failed", exc_info=True) + components = sbom.get("components") + kept = len(components) if isinstance(components, list) else 0 + return ScopeFilterResult(applied=False, kept_components=kept) + + +def rewrite_sbom_file(path: Path, sbom: dict[str, Any]) -> bool: + """Atomically rewrite the on-disk SBOM (temp file + ``os.replace``). + + Returns ``False`` (after logging) on any error — the original file is + left intact, so a failed rewrite degrades to "filter skipped". + """ + tmp_path: str | None = None + try: + fd, tmp_path = tempfile.mkstemp( + dir=str(path.parent), prefix=path.name, suffix=".tmp" + ) + with os.fdopen(fd, "w", encoding="utf-8") as handle: + json.dump(sbom, handle, ensure_ascii=False) + os.replace(tmp_path, path) + tmp_path = None + return True + except Exception: # noqa: BLE001 — degrade, never break the scan + log.warning("scope_filter_rewrite_failed", path=str(path), exc_info=True) + return False + finally: + if tmp_path is not None: + try: + os.unlink(tmp_path) + except OSError: + pass + + +# --------------------------------------------------------------------------- +# Internals +# --------------------------------------------------------------------------- + + +def _filter( + sbom: dict[str, Any], + *, + npm_lock: NpmLockfileData | None, + maven: bool, + node: bool, +) -> ScopeFilterResult: + components = sbom.get("components") + if not isinstance(components, list): + return ScopeFilterResult(applied=False) + + # Guards — each ecosystem predicate activates only when the data to + # filter *safely* is present (BomLens hasScopes parity + hasDev). + maven_active = maven and _has_maven_scopes(components) + node_active = node and _has_dev_entries(npm_lock) + + if not maven_active and not node_active: + return ScopeFilterResult(applied=False, kept_components=len(components)) + + kept: list[Any] = [] + dropped: dict[str, int] = {} + for component in components: + if not isinstance(component, dict): + kept.append(component) # defensive: never drop what we can't read + continue + purl = component.get("purl") + purl = purl if isinstance(purl, str) else "" + if maven_active and purl.startswith("pkg:maven/"): + if component.get("scope") in _MAVEN_DROP_SCOPES: + dropped["maven"] = dropped.get("maven", 0) + 1 + continue + if node_active and purl.startswith("pkg:npm/"): + assert npm_lock is not None # implied by node_active + if npm_lock.scope_for_purl(purl) == "dev": + dropped["npm"] = dropped.get("npm", 0) + 1 + continue + kept.append(component) + + if not dropped: + # Predicates ran but found nothing non-deployable — no mutation. + return ScopeFilterResult(applied=True, kept_components=len(components)) + + sbom["components"] = kept + _prune_dependencies(sbom, _kept_refs(sbom, kept)) + _stamp_filter_property(sbom, dropped) + return ScopeFilterResult( + applied=True, dropped=dropped, kept_components=len(kept) + ) + + +def _has_maven_scopes(components: list[Any]) -> bool: + """hasScopes guard — at least one maven component tagged ``required``.""" + for component in components: + if not isinstance(component, dict): + continue + purl = component.get("purl") + if ( + isinstance(purl, str) + and purl.startswith("pkg:maven/") + and component.get("scope") == "required" + ): + return True + return False + + +def _has_dev_entries(npm_lock: NpmLockfileData | None) -> bool: + """hasDev guard — the lockfile parsed AND classified something ``dev``.""" + if npm_lock is None: + return False + return any(scope == "dev" for scope in npm_lock.scope_by_purl.values()) + + +def _kept_refs(sbom: dict[str, Any], kept: list[Any]) -> set[str]: + """Refs of every kept component, plus the document root ref (always kept).""" + refs: set[str] = set() + for component in kept: + if not isinstance(component, dict): + continue + ref = component.get("bom-ref") or component.get("purl") + if isinstance(ref, str) and ref: + refs.add(ref) + metadata = sbom.get("metadata") + if isinstance(metadata, dict): + root = metadata.get("component") + if isinstance(root, dict): + root_ref = root.get("bom-ref") or root.get("purl") + if isinstance(root_ref, str) and root_ref: + refs.add(root_ref) + return refs + + +def _prune_dependencies(sbom: dict[str, Any], kept_refs: set[str]) -> None: + """Drop graph entries for removed components; filter each ``dependsOn``.""" + dependencies = sbom.get("dependencies") + if not isinstance(dependencies, list): + return + pruned: list[Any] = [] + for entry in dependencies: + if not isinstance(entry, dict): + pruned.append(entry) + continue + ref = entry.get("ref") + if isinstance(ref, str) and ref not in kept_refs: + continue + depends_on = entry.get("dependsOn") + if isinstance(depends_on, list): + entry["dependsOn"] = [ + child + for child in depends_on + if isinstance(child, str) and child in kept_refs + ] + pruned.append(entry) + sbom["dependencies"] = pruned + + +def _stamp_filter_property(sbom: dict[str, Any], dropped: dict[str, int]) -> None: + """Idempotently stamp ``trusca:scope_filter`` into ``metadata.properties``.""" + metadata = sbom.setdefault("metadata", {}) + if not isinstance(metadata, dict): + return + properties = metadata.setdefault("properties", []) + if not isinstance(properties, list): + return + properties[:] = [ + prop + for prop in properties + if not (isinstance(prop, dict) and prop.get("name") == FILTER_PROPERTY_NAME) + ] + value = ",".join(f"{eco}={count}" for eco, count in sorted(dropped.items())) + properties.append({"name": FILTER_PROPERTY_NAME, "value": value}) + + +__all__ = [ + "FILTER_PROPERTY_NAME", + "ScopeFilterResult", + "filter_sbom_to_runtime_scope", + "rewrite_sbom_file", +] diff --git a/apps/backend/tasks/ingest_sbom.py b/apps/backend/tasks/ingest_sbom.py index 16e31331..a3baabfe 100644 --- a/apps/backend/tasks/ingest_sbom.py +++ b/apps/backend/tasks/ingest_sbom.py @@ -283,6 +283,15 @@ def _run_pipeline( # by PURL, so the component graph has to exist first. ``source_dir=None`` # because an ingested SBOM has no first-party source tree (no npm-lockfile # enrichment, no scancode detections). + # + # DELIBERATE divergence from the source pipeline: the runtime-scope + # post-filter (Phase K, ``_apply_scope_filter`` in tasks/scan_source.py) + # does NOT run here. An uploaded SBOM is the supplier's *declared truth* — + # silently shrinking it would corrupt the conformance verdict already + # scored on the original bytes above, and producers that filter (e.g. + # BomLens) have already done so before upload. There is also no source + # tree, so the Node lockfile predicate could not run anyway. Pinned by + # tests/unit/tasks/test_ingest_sbom_no_scope_filter.py. _set_stage(scan_uuid, "components") with sync_session_scope() as session: persist_sbom_components( diff --git a/apps/backend/tasks/scan_source.py b/apps/backend/tasks/scan_source.py index ef1bbdb1..e687fd9b 100644 --- a/apps/backend/tasks/scan_source.py +++ b/apps/backend/tasks/scan_source.py @@ -49,6 +49,7 @@ from __future__ import annotations +import copy import hashlib import json import re @@ -72,6 +73,9 @@ from core.config import ( cdxgen_fetch_license, cdxgen_spec_version, + scan_scope_filter_enabled, + scan_scope_filter_maven_enabled, + scan_scope_filter_node_enabled, scan_soft_time_limit_seconds, scanoss_enabled, slsa_builder_id, @@ -84,7 +88,7 @@ from integrations import attestation as attestation_builder from integrations import cdxgen as cdxgen_adapter from integrations import cosign as cosign_adapter -from integrations import scan_executor +from integrations import sbom_scope_filter, scan_executor from integrations import scancode as scancode_adapter from integrations import scanoss as scanoss_adapter from integrations._size_guard import enforce_jsonb_row_size_limit @@ -393,6 +397,16 @@ def _run_pipeline( cdxgen_result = cdxgen_adapter.CdxgenResult( sbom_path=gen_result.sbom_path, sbom=gen_result.sbom ) + + # Stage 3.25 — runtime-scope post-filter (Phase K). MUST run before the + # artifact persist, cosign signing, SCANOSS and Trivy below so every + # downstream consumer (persisted components, the signed/downloadable + # artifact, vulnerability matching) sees ONE consistent filtered document. + # Best-effort: any failure leaves the SBOM exactly as cdxgen wrote it. + _apply_scope_filter( + scan_uuid=scan_uuid, cdxgen_result=cdxgen_result, source_dir=source_dir + ) + _persist_artifact( scan_uuid, kind="sbom_cyclonedx", @@ -1134,6 +1148,81 @@ def _detect_and_record_env(scan_uuid: uuid.UUID, source_dir: Path) -> str: return detected_env +def _apply_scope_filter( + *, + scan_uuid: uuid.UUID, + cdxgen_result: cdxgen_adapter.CdxgenResult, + source_dir: Path, +) -> None: + """Filter the cdxgen SBOM down to the deployable runtime set (Phase K). + + Copy-then-commit transactional shape: the filter runs on a deep copy, the + on-disk file (the same file Trivy later reads) is rewritten atomically, + and only after that succeeds is the in-memory document swapped. A failed + rewrite therefore degrades to "filter skipped" with memory and disk still + consistent — persist, signing and Trivy can never see diverging documents. + Best-effort throughout: no failure here may fail the scan. + """ + if not scan_scope_filter_enabled(): + return + try: + npm_lock = read_lockfile(source_dir) + working = copy.deepcopy(cdxgen_result.sbom) + result = sbom_scope_filter.filter_sbom_to_runtime_scope( + working, + npm_lock=npm_lock, + maven=scan_scope_filter_maven_enabled(), + node=scan_scope_filter_node_enabled(), + ) + if not result.applied: + return + if result.dropped: + if not sbom_scope_filter.rewrite_sbom_file( + cdxgen_result.sbom_path, working + ): + return + # Same object identity — CdxgenResult stays valid either way. + cdxgen_result.sbom.clear() + cdxgen_result.sbom.update(working) + log.info( + "scope_filter_applied", + scan_id=str(scan_uuid), + dropped=dict(result.dropped), + kept=result.kept_components, + ) + _record_scope_filter(scan_uuid, result) + except Exception: # noqa: BLE001 — the filter must never fail the scan + log.warning( + "scope_filter_stage_failed", scan_id=str(scan_uuid), exc_info=True + ) + + +def _record_scope_filter( + scan_uuid: uuid.UUID, result: sbom_scope_filter.ScopeFilterResult +) -> None: + """Persist the filter outcome onto ``scan_metadata`` (best-effort). + + Same JSONB reassign-fresh-dict pattern as ``_detect_and_record_env`` — + in-place mutation is not tracked by SQLAlchemy. + """ + try: + with sync_session_scope() as session: + scan = session.get(Scan, scan_uuid) + if scan is not None: + merged = dict(scan.scan_metadata or {}) + merged["scope_filter"] = { + "applied": True, + "dropped": dict(result.dropped), + "kept": result.kept_components, + } + scan.scan_metadata = merged + session.commit() + except Exception: # noqa: BLE001 — telemetry is best-effort, never fatal + log.warning( + "scope_filter_persist_failed", scan_id=str(scan_uuid), exc_info=True + ) + + def _persist_artifact( scan_uuid: uuid.UUID, *, kind: str, path: Path, sha256: str | None = None ) -> None: diff --git a/apps/backend/tests/fixtures/npm/package-lock.devdeps.json b/apps/backend/tests/fixtures/npm/package-lock.devdeps.json new file mode 100644 index 00000000..f10a03b4 --- /dev/null +++ b/apps/backend/tests/fixtures/npm/package-lock.devdeps.json @@ -0,0 +1,5526 @@ +{ + "name": "sbom-example-nodejs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sbom-example-nodejs", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.6.2", + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "morgan": "^1.10.0", + "winston": "^3.11.0" + }, + "devDependencies": { + "eslint": "^8.55.0", + "jest": "^29.7.0", + "nodemon": "^3.0.2", + "prettier": "^3.1.1", + "supertest": "^6.3.3" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "26.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "dev": true, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "6.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "5.0.3", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.380", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.11.0", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.4.1", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.50", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.5", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.0", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/apps/backend/tests/fixtures/sbom/real_cyclonedx_maven_scoped.json b/apps/backend/tests/fixtures/sbom/real_cyclonedx_maven_scoped.json new file mode 100644 index 00000000..0cfa6df8 --- /dev/null +++ b/apps/backend/tests/fixtures/sbom/real_cyclonedx_maven_scoped.json @@ -0,0 +1 @@ +{"bomFormat":"CycloneDX","specVersion":"1.5","serialNumber":"urn:uuid:4d8c5e5a-33e3-4595-a071-49cb66777cce","version":1,"metadata":{"timestamp":"2026-07-10T15:03:03Z","tools":{"components":[{"group":"@cyclonedx","name":"cdxgen","version":"12.3.3","purl":"pkg:npm/%40cyclonedx/cdxgen@12.3.3","type":"application","bom-ref":"pkg:npm/@cyclonedx/cdxgen@12.3.3","author":"OWASP Foundation","publisher":"OWASP Foundation"}]},"authors":[{"name":"OWASP Foundation"}],"lifecycles":[{"phase":"build"}],"component":{"group":"com.example","name":"sbom-example-app","version":"1.0.0","properties":[{"name":"SrcFile","value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}],"purl":"pkg:maven/com.example/sbom-example-app@1.0.0?type=jar","bom-ref":"pkg:maven/com.example/sbom-example-app@1.0.0?type=jar","type":"application"},"properties":[{"name":"cdx:bom:componentTypes","value":"chrome-extension\\nmaven"},{"name":"cdx:bom:componentNamespaces","value":"ch.qos.logback\\ncom.fasterxml\\ncom.fasterxml.jackson.core\\ncom.fasterxml.jackson.datatype\\ncom.fasterxml.jackson.module\\ncom.h2database\\ncom.jayway.jsonpath\\ncom.sun.istack\\ncom.vaadin.external.google\\ncom.zaxxer\\nio.micrometer\\nio.smallrye\\njakarta.activation\\njakarta.annotation\\njakarta.inject\\njakarta.persistence\\njakarta.transaction\\njakarta.xml.bind\\nnet.bytebuddy\\nnet.minidev\\norg.antlr\\norg.apache.commons\\norg.apache.logging.log4j\\norg.apache.tomcat.embed\\norg.apiguardian\\norg.aspectj\\norg.assertj\\norg.awaitility\\norg.eclipse.angus\\norg.glassfish.jaxb\\norg.hamcrest\\norg.hibernate.common\\norg.hibernate.orm\\norg.jboss.logging\\norg.junit.jupiter\\norg.junit.platform\\norg.mockito\\norg.objenesis\\norg.opentest4j\\norg.ow2.asm\\norg.projectlombok\\norg.skyscreamer\\norg.slf4j\\norg.springframework\\norg.springframework.boot\\norg.springframework.data\\norg.xmlunit\\norg.yaml"},{"name":"cdx:bom:componentSrcFiles","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/cfhdojbkjhnklbpkdaibdccddilifddb/4.41.2_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/egaejpfbkjamgheoingidhokbfnidlpi/1.6_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/emnphkkblegpebimobpbekeedfgemhof/1.6.10_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/fdpohaocaechififmbbbbbknoalclacl/8.6_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.107.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hahkjjkedonglpienpfiganogikkkoii/1.7_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hmjkmjkepdijhoojdojkdfohbdgmmhki/4.26091.600.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hoombieeljmmljlkjmnheibnpciblicm/5.1.8_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ijdefdijdmghafocfmmdojfghnpelnfn/3.33.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/inomeogfingihgjfjlpeplalcfajhgai/2.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/kobncfkmjelbefaoohoblamnbackjggk/1.0.30_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/lkcdojpmjehlniamnglpjlldkoonlomb/1.0.0.5_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ndnaehgpjlnokgebbaldlmgkapkpjkkb/12.82.0_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nlbgclfdbneidpjhchcipedhbffffghm/1.0.1.7_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nmmhkkegccagdldgiimedpiccmgmieda/1.0.0.6_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/oeopbcgkkoapgobdbedcemjljbihmemj/36.2_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Profile 1/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.104.1_0/manifest.json\\npom.xml"}]},"components":[{"group":"org.springframework.boot","name":"spring-boot-starter-web","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-autoconfigure","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter-logging","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"ch.qos.logback","name":"logback-classic","version":"1.4.11","scope":"required","purl":"pkg:maven/ch.qos.logback/logback-classic@1.4.11?type=jar","type":"library","bom-ref":"pkg:maven/ch.qos.logback/logback-classic@1.4.11?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"ch.qos.logback","name":"logback-core","version":"1.4.11","scope":"required","purl":"pkg:maven/ch.qos.logback/logback-core@1.4.11?type=jar","type":"library","bom-ref":"pkg:maven/ch.qos.logback/logback-core@1.4.11?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.apache.logging.log4j","name":"log4j-to-slf4j","version":"2.21.1","scope":"required","purl":"pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.21.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.21.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.apache.logging.log4j","name":"log4j-api","version":"2.21.1","scope":"required","purl":"pkg:maven/org.apache.logging.log4j/log4j-api@2.21.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.apache.logging.log4j/log4j-api@2.21.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.slf4j","name":"jul-to-slf4j","version":"2.0.9","scope":"required","purl":"pkg:maven/org.slf4j/jul-to-slf4j@2.0.9?type=jar","type":"framework","bom-ref":"pkg:maven/org.slf4j/jul-to-slf4j@2.0.9?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"jakarta.annotation","name":"jakarta.annotation-api","version":"2.1.1","scope":"required","purl":"pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar","type":"library","bom-ref":"pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.yaml","name":"snakeyaml","version":"2.2","scope":"required","purl":"pkg:maven/org.yaml/snakeyaml@2.2?type=jar","type":"library","bom-ref":"pkg:maven/org.yaml/snakeyaml@2.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter-json","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-json@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-json@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.fasterxml.jackson.datatype","name":"jackson-datatype-jdk8","version":"2.15.3","scope":"required","purl":"pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.15.3?type=jar","type":"library","bom-ref":"pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.15.3?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.fasterxml.jackson.datatype","name":"jackson-datatype-jsr310","version":"2.15.3","scope":"required","purl":"pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.15.3?type=jar","type":"library","bom-ref":"pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.15.3?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.fasterxml.jackson.module","name":"jackson-module-parameter-names","version":"2.15.3","scope":"required","purl":"pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.15.3?type=jar","type":"library","bom-ref":"pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.15.3?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter-tomcat","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.apache.tomcat.embed","name":"tomcat-embed-core","version":"10.1.16","scope":"required","purl":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.16?type=jar","type":"framework","bom-ref":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.16?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.apache.tomcat.embed","name":"tomcat-embed-el","version":"10.1.16","scope":"required","purl":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.16?type=jar","type":"framework","bom-ref":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.16?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.apache.tomcat.embed","name":"tomcat-embed-websocket","version":"10.1.16","scope":"required","purl":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.16?type=jar","type":"framework","bom-ref":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.16?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-web","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-web@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-web@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-beans","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-beans@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-beans@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"io.micrometer","name":"micrometer-observation","version":"1.12.0","scope":"required","purl":"pkg:maven/io.micrometer/micrometer-observation@1.12.0?type=jar","type":"library","bom-ref":"pkg:maven/io.micrometer/micrometer-observation@1.12.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"io.micrometer","name":"micrometer-commons","version":"1.12.0","scope":"required","purl":"pkg:maven/io.micrometer/micrometer-commons@1.12.0?type=jar","type":"library","bom-ref":"pkg:maven/io.micrometer/micrometer-commons@1.12.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-webmvc","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-webmvc@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-webmvc@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-aop","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-aop@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-aop@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-context","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-context@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-context@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-expression","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-expression@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-expression@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter-data-jpa","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-data-jpa@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-data-jpa@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter-aop","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-aop@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-aop@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.aspectj","name":"aspectjweaver","version":"1.9.20.1","scope":"required","purl":"pkg:maven/org.aspectj/aspectjweaver@1.9.20.1?type=jar","type":"library","bom-ref":"pkg:maven/org.aspectj/aspectjweaver@1.9.20.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter-jdbc","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-jdbc@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-jdbc@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.zaxxer","name":"HikariCP","version":"5.0.1","scope":"required","purl":"pkg:maven/com.zaxxer/HikariCP@5.0.1?type=jar","type":"library","bom-ref":"pkg:maven/com.zaxxer/HikariCP@5.0.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-jdbc","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-jdbc@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-jdbc@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.hibernate.orm","name":"hibernate-core","version":"6.3.1.Final","scope":"required","purl":"pkg:maven/org.hibernate.orm/hibernate-core@6.3.1.Final?type=jar","type":"framework","bom-ref":"pkg:maven/org.hibernate.orm/hibernate-core@6.3.1.Final?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"jakarta.persistence","name":"jakarta.persistence-api","version":"3.1.0","scope":"required","purl":"pkg:maven/jakarta.persistence/jakarta.persistence-api@3.1.0?type=jar","type":"library","bom-ref":"pkg:maven/jakarta.persistence/jakarta.persistence-api@3.1.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"jakarta.transaction","name":"jakarta.transaction-api","version":"2.0.1","scope":"required","purl":"pkg:maven/jakarta.transaction/jakarta.transaction-api@2.0.1?type=jar","type":"library","bom-ref":"pkg:maven/jakarta.transaction/jakarta.transaction-api@2.0.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.jboss.logging","name":"jboss-logging","version":"3.5.0.Final","scope":"required","purl":"pkg:maven/org.jboss.logging/jboss-logging@3.5.0.Final?type=jar","type":"library","bom-ref":"pkg:maven/org.jboss.logging/jboss-logging@3.5.0.Final?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.hibernate.common","name":"hibernate-commons-annotations","version":"6.0.6.Final","scope":"required","purl":"pkg:maven/org.hibernate.common/hibernate-commons-annotations@6.0.6.Final?type=jar","type":"framework","bom-ref":"pkg:maven/org.hibernate.common/hibernate-commons-annotations@6.0.6.Final?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"io.smallrye","name":"jandex","version":"3.1.2","scope":"required","purl":"pkg:maven/io.smallrye/jandex@3.1.2?type=jar","type":"framework","bom-ref":"pkg:maven/io.smallrye/jandex@3.1.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.fasterxml","name":"classmate","version":"1.5.1","scope":"required","purl":"pkg:maven/com.fasterxml/classmate@1.5.1?type=jar","type":"library","bom-ref":"pkg:maven/com.fasterxml/classmate@1.5.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"net.bytebuddy","name":"byte-buddy","version":"1.14.7","scope":"required","purl":"pkg:maven/net.bytebuddy/byte-buddy@1.14.7?type=jar","type":"library","bom-ref":"pkg:maven/net.bytebuddy/byte-buddy@1.14.7?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.glassfish.jaxb","name":"jaxb-runtime","version":"4.0.2","scope":"required","purl":"pkg:maven/org.glassfish.jaxb/jaxb-runtime@4.0.2?type=jar","type":"library","bom-ref":"pkg:maven/org.glassfish.jaxb/jaxb-runtime@4.0.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.glassfish.jaxb","name":"jaxb-core","version":"4.0.2","scope":"required","purl":"pkg:maven/org.glassfish.jaxb/jaxb-core@4.0.2?type=jar","type":"library","bom-ref":"pkg:maven/org.glassfish.jaxb/jaxb-core@4.0.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.eclipse.angus","name":"angus-activation","version":"2.0.0","scope":"required","purl":"pkg:maven/org.eclipse.angus/angus-activation@2.0.0?type=jar","type":"library","bom-ref":"pkg:maven/org.eclipse.angus/angus-activation@2.0.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.glassfish.jaxb","name":"txw2","version":"4.0.2","scope":"required","purl":"pkg:maven/org.glassfish.jaxb/txw2@4.0.2?type=jar","type":"library","bom-ref":"pkg:maven/org.glassfish.jaxb/txw2@4.0.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.sun.istack","name":"istack-commons-runtime","version":"4.1.1","scope":"required","purl":"pkg:maven/com.sun.istack/istack-commons-runtime@4.1.1?type=jar","type":"library","bom-ref":"pkg:maven/com.sun.istack/istack-commons-runtime@4.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"jakarta.inject","name":"jakarta.inject-api","version":"2.0.1","scope":"required","purl":"pkg:maven/jakarta.inject/jakarta.inject-api@2.0.1?type=jar","type":"library","bom-ref":"pkg:maven/jakarta.inject/jakarta.inject-api@2.0.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.antlr","name":"antlr4-runtime","version":"4.10.1","scope":"required","purl":"pkg:maven/org.antlr/antlr4-runtime@4.10.1?type=jar","type":"library","bom-ref":"pkg:maven/org.antlr/antlr4-runtime@4.10.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.data","name":"spring-data-jpa","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.data/spring-data-jpa@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.data/spring-data-jpa@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.data","name":"spring-data-commons","version":"3.2.0","scope":"required","purl":"pkg:maven/org.springframework.data/spring-data-commons@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.data/spring-data-commons@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-orm","version":"6.1.0","scope":"required","purl":"pkg:maven/org.springframework/spring-orm@6.1.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-orm@6.1.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-tx","version":"6.1.0","scope":"required","purl":"pkg:maven/org.springframework/spring-tx@6.1.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-tx@6.1.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.slf4j","name":"slf4j-api","version":"2.0.2","scope":"required","purl":"pkg:maven/org.slf4j/slf4j-api@2.0.2?type=jar","type":"framework","bom-ref":"pkg:maven/org.slf4j/slf4j-api@2.0.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-aspects","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-aspects@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-aspects@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.h2database","name":"h2","version":"2.2.224","scope":"required","purl":"pkg:maven/com.h2database/h2@2.2.224?type=jar","type":"library","bom-ref":"pkg:maven/com.h2database/h2@2.2.224?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.projectlombok","name":"lombok","version":"1.18.30","scope":"excluded","purl":"pkg:maven/org.projectlombok/lombok@1.18.30?type=jar","type":"library","bom-ref":"pkg:maven/org.projectlombok/lombok@1.18.30?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"provided"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.apache.commons","name":"commons-lang3","version":"3.14.0","scope":"required","purl":"pkg:maven/org.apache.commons/commons-lang3@3.14.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.apache.commons/commons-lang3@3.14.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.fasterxml.jackson.core","name":"jackson-databind","version":"2.16.0","scope":"required","purl":"pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.16.0?type=jar","type":"library","bom-ref":"pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.16.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.fasterxml.jackson.core","name":"jackson-annotations","version":"2.16.0","scope":"required","purl":"pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.16.0?type=jar","type":"library","bom-ref":"pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.16.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.fasterxml.jackson.core","name":"jackson-core","version":"2.16.0","scope":"required","purl":"pkg:maven/com.fasterxml.jackson.core/jackson-core@2.16.0?type=jar","type":"library","bom-ref":"pkg:maven/com.fasterxml.jackson.core/jackson-core@2.16.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-starter-test","version":"3.2.0","scope":"optional","purl":"pkg:maven/org.springframework.boot/spring-boot-starter-test@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-starter-test@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-test","version":"3.2.0","scope":"optional","purl":"pkg:maven/org.springframework.boot/spring-boot-test@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-test@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework.boot","name":"spring-boot-test-autoconfigure","version":"3.2.0","scope":"optional","purl":"pkg:maven/org.springframework.boot/spring-boot-test-autoconfigure@3.2.0?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework.boot/spring-boot-test-autoconfigure@3.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.jayway.jsonpath","name":"json-path","version":"2.8.0","scope":"optional","purl":"pkg:maven/com.jayway.jsonpath/json-path@2.8.0?type=jar","type":"library","bom-ref":"pkg:maven/com.jayway.jsonpath/json-path@2.8.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"jakarta.xml.bind","name":"jakarta.xml.bind-api","version":"4.0.1","scope":"required","purl":"pkg:maven/jakarta.xml.bind/jakarta.xml.bind-api@4.0.1?type=jar","type":"library","bom-ref":"pkg:maven/jakarta.xml.bind/jakarta.xml.bind-api@4.0.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"jakarta.activation","name":"jakarta.activation-api","version":"2.1.2","scope":"required","purl":"pkg:maven/jakarta.activation/jakarta.activation-api@2.1.2?type=jar","type":"library","bom-ref":"pkg:maven/jakarta.activation/jakarta.activation-api@2.1.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"runtime"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"net.minidev","name":"json-smart","version":"2.5.0","scope":"optional","purl":"pkg:maven/net.minidev/json-smart@2.5.0?type=jar","type":"library","bom-ref":"pkg:maven/net.minidev/json-smart@2.5.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"net.minidev","name":"accessors-smart","version":"2.5.0","scope":"optional","purl":"pkg:maven/net.minidev/accessors-smart@2.5.0?type=jar","type":"library","bom-ref":"pkg:maven/net.minidev/accessors-smart@2.5.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.ow2.asm","name":"asm","version":"9.3","scope":"optional","purl":"pkg:maven/org.ow2.asm/asm@9.3?type=jar","type":"framework","bom-ref":"pkg:maven/org.ow2.asm/asm@9.3?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.assertj","name":"assertj-core","version":"3.24.2","scope":"optional","purl":"pkg:maven/org.assertj/assertj-core@3.24.2?type=jar","type":"library","bom-ref":"pkg:maven/org.assertj/assertj-core@3.24.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.awaitility","name":"awaitility","version":"4.2.0","scope":"optional","purl":"pkg:maven/org.awaitility/awaitility@4.2.0?type=jar","type":"library","bom-ref":"pkg:maven/org.awaitility/awaitility@4.2.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.hamcrest","name":"hamcrest","version":"2.2","scope":"optional","purl":"pkg:maven/org.hamcrest/hamcrest@2.2?type=jar","type":"library","bom-ref":"pkg:maven/org.hamcrest/hamcrest@2.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.junit.jupiter","name":"junit-jupiter","version":"5.10.1","scope":"optional","purl":"pkg:maven/org.junit.jupiter/junit-jupiter@5.10.1?type=jar","type":"library","bom-ref":"pkg:maven/org.junit.jupiter/junit-jupiter@5.10.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.junit.jupiter","name":"junit-jupiter-api","version":"5.10.1","scope":"optional","purl":"pkg:maven/org.junit.jupiter/junit-jupiter-api@5.10.1?type=jar","type":"library","bom-ref":"pkg:maven/org.junit.jupiter/junit-jupiter-api@5.10.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.opentest4j","name":"opentest4j","version":"1.3.0","scope":"optional","purl":"pkg:maven/org.opentest4j/opentest4j@1.3.0?type=jar","type":"library","bom-ref":"pkg:maven/org.opentest4j/opentest4j@1.3.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.junit.platform","name":"junit-platform-commons","version":"1.10.1","scope":"optional","purl":"pkg:maven/org.junit.platform/junit-platform-commons@1.10.1?type=jar","type":"library","bom-ref":"pkg:maven/org.junit.platform/junit-platform-commons@1.10.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.apiguardian","name":"apiguardian-api","version":"1.1.2","scope":"optional","purl":"pkg:maven/org.apiguardian/apiguardian-api@1.1.2?type=jar","type":"library","bom-ref":"pkg:maven/org.apiguardian/apiguardian-api@1.1.2?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.junit.jupiter","name":"junit-jupiter-params","version":"5.10.1","scope":"optional","purl":"pkg:maven/org.junit.jupiter/junit-jupiter-params@5.10.1?type=jar","type":"library","bom-ref":"pkg:maven/org.junit.jupiter/junit-jupiter-params@5.10.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.junit.jupiter","name":"junit-jupiter-engine","version":"5.10.1","scope":"optional","purl":"pkg:maven/org.junit.jupiter/junit-jupiter-engine@5.10.1?type=jar","type":"library","bom-ref":"pkg:maven/org.junit.jupiter/junit-jupiter-engine@5.10.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.junit.platform","name":"junit-platform-engine","version":"1.10.1","scope":"optional","purl":"pkg:maven/org.junit.platform/junit-platform-engine@1.10.1?type=jar","type":"library","bom-ref":"pkg:maven/org.junit.platform/junit-platform-engine@1.10.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.mockito","name":"mockito-core","version":"5.7.0","scope":"optional","purl":"pkg:maven/org.mockito/mockito-core@5.7.0?type=jar","type":"library","bom-ref":"pkg:maven/org.mockito/mockito-core@5.7.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"net.bytebuddy","name":"byte-buddy-agent","version":"1.14.9","scope":"optional","purl":"pkg:maven/net.bytebuddy/byte-buddy-agent@1.14.9?type=jar","type":"library","bom-ref":"pkg:maven/net.bytebuddy/byte-buddy-agent@1.14.9?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.objenesis","name":"objenesis","version":"3.3","scope":"optional","purl":"pkg:maven/org.objenesis/objenesis@3.3?type=jar","type":"library","bom-ref":"pkg:maven/org.objenesis/objenesis@3.3?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.mockito","name":"mockito-junit-jupiter","version":"5.7.0","scope":"optional","purl":"pkg:maven/org.mockito/mockito-junit-jupiter@5.7.0?type=jar","type":"library","bom-ref":"pkg:maven/org.mockito/mockito-junit-jupiter@5.7.0?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.skyscreamer","name":"jsonassert","version":"1.5.1","scope":"optional","purl":"pkg:maven/org.skyscreamer/jsonassert@1.5.1?type=jar","type":"library","bom-ref":"pkg:maven/org.skyscreamer/jsonassert@1.5.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"com.vaadin.external.google","name":"android-json","version":"0.0.20131108.vaadin1","scope":"optional","purl":"pkg:maven/com.vaadin.external.google/android-json@0.0.20131108.vaadin1?type=jar","type":"framework","bom-ref":"pkg:maven/com.vaadin.external.google/android-json@0.0.20131108.vaadin1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-core","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-core@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-core@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-jcl","version":"6.1.1","scope":"required","purl":"pkg:maven/org.springframework/spring-jcl@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-jcl@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"compile"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.springframework","name":"spring-test","version":"6.1.1","scope":"optional","purl":"pkg:maven/org.springframework/spring-test@6.1.1?type=jar","type":"framework","bom-ref":"pkg:maven/org.springframework/spring-test@6.1.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"org.xmlunit","name":"xmlunit-core","version":"2.9.1","scope":"optional","purl":"pkg:maven/org.xmlunit/xmlunit-core@2.9.1?type=jar","type":"library","bom-ref":"pkg:maven/org.xmlunit/xmlunit-core@2.9.1?type=jar","properties":[{"name":"cdx:maven:component_scope","value":"test"},{"name":"SrcFile","value":"pom.xml"}],"evidence":{"identity":{"field":"purl","confidence":0.5,"methods":[{"technique":"manifest-analysis","confidence":0.5,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/java-maven/pom.xml"}]}}},{"group":"","name":"cfhdojbkjhnklbpkdaibdccddilifddb","version":"4.41.2","description":"__MSG_name_releasebuild__","purl":"pkg:chrome-extension/cfhdojbkjhnklbpkdaibdccddilifddb@4.41.2","type":"application","bom-ref":"pkg:chrome-extension/cfhdojbkjhnklbpkdaibdccddilifddb@4.41.2","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"alarms, contextMenus, declarativeNetRequestWithHostAccess, notifications, scripting, storage, tabs, unlimitedStorage, webNavigation, webRequest"},{"name":"cdx:chrome-extension:optionalPermissions","value":"contentSettings, management"},{"name":"cdx:chrome-extension:hostPermissions","value":", http://*/*, https://*/*, *://*.youtube.com/*, https://accounts.adblockplus.org/*, https://*.myaccount.adblockplus.org/*, https://adblockplus.org/*, https://new.adblockplus.org/*, https://welcome.adblockplus.org/*, https://getadblock.com/*, https://vpn.getadblock.com/*, https://*.myaccount.getadblock.com/*, https://btloader.com/trustedIframe.html, *://*.facebook.com/*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start, document_idle"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"managed-storage-schema.json"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"124.0"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/cfhdojbkjhnklbpkdaibdccddilifddb/4.41.2_0/manifest.json"}]},{"group":"","name":"egaejpfbkjamgheoingidhokbfnidlpi","version":"1.6","description":"Symantec Extension","purl":"pkg:chrome-extension/egaejpfbkjamgheoingidhokbfnidlpi@1.6","type":"application","bom-ref":"pkg:chrome-extension/egaejpfbkjamgheoingidhokbfnidlpi@1.6","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"nativeMessaging, tabs, scripting"},{"name":"cdx:chrome-extension:hostPermissions","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/egaejpfbkjamgheoingidhokbfnidlpi/1.6_0/manifest.json"}]},{"group":"","name":"emnphkkblegpebimobpbekeedfgemhof","version":"1.6.10","description":"Native HLS Playback","purl":"pkg:chrome-extension/emnphkkblegpebimobpbekeedfgemhof@1.6.10","type":"application","bom-ref":"pkg:chrome-extension/emnphkkblegpebimobpbekeedfgemhof@1.6.10","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"tabs, storage, scripting, activeTab, webNavigation"},{"name":"cdx:chrome-extension:hostPermissions","value":""},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'; connect-src *\"}"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/emnphkkblegpebimobpbekeedfgemhof/1.6.10_0/manifest.json"}]},{"group":"","name":"fdpohaocaechififmbbbbbknoalclacl","version":"8.6","description":"GoFullPage - Full Page Screen Capture","purl":"pkg:chrome-extension/fdpohaocaechififmbbbbbknoalclacl@8.6","type":"application","bom-ref":"pkg:chrome-extension/fdpohaocaechififmbbbbbknoalclacl@8.6","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"activeTab, scripting, storage, unlimitedStorage"},{"name":"cdx:chrome-extension:optionalPermissions","value":"downloads, webNavigation"},{"name":"cdx:chrome-extension:optionalHostPermissions","value":", file://*/*"},{"name":"cdx:chrome-extension:commands","value":"_execute_action"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"102"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":"*://*.gofullpage.com/*, https://localhost:1234/*, https://dev.d32cgdvim65k7p.amplifyapp.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"fileAccess, network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:fileAccess","value":"true"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/fdpohaocaechififmbbbbbknoalclacl/8.6_0/manifest.json"}]},{"group":"","name":"ghbmnnjooekpmoecnnnilnnbdlolhkhi","version":"1.107.1","description":"__MSG_extName__","purl":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.107.1","type":"application","bom-ref":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.107.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"alarms, storage, unlimitedStorage, offscreen"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://docs.google.com/*, https://drive.google.com/*"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'\"}"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"dasherSettingSchema.json"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"88"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:externallyConnectableMatches","value":"https://docs.google.com/*, https://drive.google.com/*, https://drive-autopush.corp.google.com/*, https://drive-daily-0.corp.google.com/*, https://drive-daily-1.corp.google.com/*, https://drive-daily-2.corp.google.com/*, https://drive-daily-3.corp.google.com/*, https://drive-daily-4.corp.google.com/*, https://drive-daily-5.corp.google.com/*, https://drive-daily-6.corp.google.com/*, https://drive-preprod.corp.google.com/*, https://drive-staging.corp.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.107.1_0/manifest.json"}]},{"group":"","name":"hahkjjkedonglpienpfiganogikkkoii","version":"1.7","description":"Play HLS","purl":"pkg:chrome-extension/hahkjjkedonglpienpfiganogikkkoii@1.7","type":"application","bom-ref":"pkg:chrome-extension/hahkjjkedonglpienpfiganogikkkoii@1.7","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"*://*/*m3u8*, *://*/*.ts*, webRequest, webRequestBlocking"},{"name":"cdx:chrome-extension:hostPermissions","value":"*://*/*.m3u8*, *://*/*.ts*"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hahkjjkedonglpienpfiganogikkkoii/1.7_0/manifest.json"}]},{"group":"","name":"hmjkmjkepdijhoojdojkdfohbdgmmhki","version":"4.26091.600.1","description":"__MSG_appName__","purl":"pkg:chrome-extension/hmjkmjkepdijhoojdojkdfohbdgmmhki@4.26091.600.1","type":"application","bom-ref":"pkg:chrome-extension/hmjkmjkepdijhoojdojkdfohbdgmmhki@4.26091.600.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"fileSystem, identity, identity.email, unlimitedStorage, https://*.googleapis.com/, https://keep.google.com/media/, https://*.googleusercontent.com/, https://*.client-channel.google.com/client-channel, https://clients4.google.com/client-channel/client, https://www.google-analytics.com/, https://www.google.com/, https://play.google.com/log, geolocation, management, notifications"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"57"},{"name":"cdx:chrome-extension:offlineEnabled","value":"true"},{"name":"cdx:chrome-extension:capabilities","value":"fileAccess, network, fingerprinting"},{"name":"cdx:chrome-extension:capability:fileAccess","value":"true"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hmjkmjkepdijhoojdojkdfohbdgmmhki/4.26091.600.1_0/manifest.json"}]},{"group":"","name":"hoombieeljmmljlkjmnheibnpciblicm","version":"5.1.8","description":"Language Reactor","purl":"pkg:chrome-extension/hoombieeljmmljlkjmnheibnpciblicm@5.1.8","type":"application","bom-ref":"pkg:chrome-extension/hoombieeljmmljlkjmnheibnpciblicm@5.1.8","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"storage, contextMenus, activeTab, scripting"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://i.ytimg.com/*, https://*.netflix.com/*, *://*.youtube.com/*, *://*.amazon.com.br/*, *://*.amazon.ca/*, *://*.amazon.com.mx/*, *://*.amazon.com/*, *://*.amazon.cn/*, *://*.amazon.in/*, *://*.amazon.co.jp/*, *://*.amazon.sg/*, *://*.amazon.ae/*, *://*.amazon.sa/*, *://*.amazon.fr/*, *://*.amazon.de/*, *://*.amazon.it/*, *://*.amazon.nl/*, *://*.amazon.pl/*, *://*.amazon.es/*, *://*.amazon.se/*, *://*.amazon.com.tr/*, *://*.amazon.co.uk/*, *://*.amazon.com.au/*, https://*.languagereactor.com/*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'; img-src 'self' https://www.gstatic.com/ https://cdn.firebase.com;/ connect-src 'self' https://*.firebaseio.com/ https://api.dioco.io/ https://www.youtube.com/ https://www.googleapis.com/ https://securetoken.googleapis.com/ https://us-central1-nlle-b0128.cloudfunctions.net/ https://apis.google.com/ https://identitytoolkit.googleapis.com/ https://i.ytimg.com/\"}"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hoombieeljmmljlkjmnheibnpciblicm/5.1.8_0/manifest.json"}]},{"group":"","name":"ijdefdijdmghafocfmmdojfghnpelnfn","version":"3.33.1","description":"__MSG_extName__","purl":"pkg:chrome-extension/ijdefdijdmghafocfmmdojfghnpelnfn@3.33.1","type":"application","bom-ref":"pkg:chrome-extension/ijdefdijdmghafocfmmdojfghnpelnfn@3.33.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"activeTab, storage, cookies, scripting, declarativeNetRequestWithHostAccess"},{"name":"cdx:chrome-extension:optionalPermissions","value":"tabs"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://notebooklm.google.com/*, https://clerk.notebooklm-web-importer.com/*"},{"name":"cdx:chrome-extension:optionalHostPermissions","value":"https://*/*, http://*/*"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ijdefdijdmghafocfmmdojfghnpelnfn/3.33.1_0/manifest.json"}]},{"group":"","name":"inomeogfingihgjfjlpeplalcfajhgai","version":"2.1","description":"Chrome Remote Desktop","purl":"pkg:chrome-extension/inomeogfingihgjfjlpeplalcfajhgai@2.1","type":"application","bom-ref":"pkg:chrome-extension/inomeogfingihgjfjlpeplalcfajhgai@2.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"nativeMessaging, downloads"},{"name":"cdx:chrome-extension:externallyConnectableMatches","value":"https://remotedesktop.corp.google.com/*, https://remotedesktop.google.com/*, https://remotedesktop-dev.corp.google.com/*, https://remotedesktop-autopush.corp.google.com/*, https://remotedesktop-daily-0.corp.google.com/*, https://remotedesktop-daily-1.corp.google.com/*, https://remotedesktop-daily-2.corp.google.com/*, https://remotedesktop-daily-3.corp.google.com/*, https://remotedesktop-daily-4.corp.google.com/*, https://remotedesktop-daily-5.corp.google.com/*, https://remotedesktop-daily-6.corp.google.com/*, https://remotesupport.corp.google.com/*, https://remotesupport-dev.corp.google.com/*, https://remotesupport-autopush.corp.google.com/*, https://remotesupport-daily-0.corp.google.com/*, https://remotesupport-daily-1.corp.google.com/*, https://remotesupport-daily-2.corp.google.com/*, https://remotesupport-daily-3.corp.google.com/*, https://remotesupport-daily-4.corp.google.com/*, https://remotesupport-daily-5.corp.google.com/*, https://remotesupport-daily-6.corp.google.com/*, https://remoting.sandbox.google.com/*, https://remoting-autopush.sandbox.google.com/*, https://remoting-daily-0.sandbox.google.com/*, https://remoting-daily-1.sandbox.google.com/*, https://remoting-daily-2.sandbox.google.com/*, https://remoting-daily-3.sandbox.google.com/*, https://remoting-daily-4.sandbox.google.com/*, https://remoting-daily-5.sandbox.google.com/*, https://remoting-daily-6.sandbox.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"fileAccess"},{"name":"cdx:chrome-extension:capability:fileAccess","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/inomeogfingihgjfjlpeplalcfajhgai/2.1_0/manifest.json"}]},{"group":"","name":"kobncfkmjelbefaoohoblamnbackjggk","version":"1.0.30","description":"__MSG_appName__","purl":"pkg:chrome-extension/kobncfkmjelbefaoohoblamnbackjggk@1.0.30","type":"application","bom-ref":"pkg:chrome-extension/kobncfkmjelbefaoohoblamnbackjggk@1.0.30","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"tabs, storage, contextMenus, scripting, unlimitedStorage"},{"name":"cdx:chrome-extension:hostPermissions","value":", *://*/*, https://*.youtube.com/*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/kobncfkmjelbefaoohoblamnbackjggk/1.0.30_0/manifest.json"}]},{"group":"","name":"lkcdojpmjehlniamnglpjlldkoonlomb","version":"1.0.0.5","description":"__MSG_ext_name__","purl":"pkg:chrome-extension/lkcdojpmjehlniamnglpjlldkoonlomb@1.0.0.5","type":"application","bom-ref":"pkg:chrome-extension/lkcdojpmjehlniamnglpjlldkoonlomb@1.0.0.5","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"http://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"notifications, unlimitedStorage"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/lkcdojpmjehlniamnglpjlldkoonlomb/1.0.0.5_0/manifest.json"}]},{"group":"","name":"ndnaehgpjlnokgebbaldlmgkapkpjkkb","version":"12.82.0","description":"__MSG_appName__","purl":"pkg:chrome-extension/ndnaehgpjlnokgebbaldlmgkapkpjkkb@12.82.0","type":"application","bom-ref":"pkg:chrome-extension/ndnaehgpjlnokgebbaldlmgkapkpjkkb@12.82.0","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"storage, declarativeNetRequestWithHostAccess, notifications"},{"name":"cdx:chrome-extension:hostPermissions","value":"*://*.mailtrack.io/*, *://mailtrack.io/*, *://*.mailsuite.com/*, *://mailsuite.com/*, https://mail.google.com/*, *://*.googleusercontent.com/*, https://api.rollbar.com/*, https://http-intake.logs.datadoghq.eu/*, https://mrt-attachments-prod.s3.eu-west-1.amazonaws.com/*, https://mt-file-tracking-temp.s3.eu-west-1.amazonaws.com/*, *://mailtrack.io/*/dashboard/welcome*, *://mailtrack.io/*/dashboard/reauthorized*, *://mailtrack.io/*/dashboard/install-success*, *://mailtrack.io/*/dashboard/payment/teams/success*, *://mailsuite.com/*/dashboard/welcome*, *://mailsuite.com/*/dashboard/reauthorized*, *://mailsuite.com/*/dashboard/install-success*, *://mailsuite.com/*/dashboard/payment/teams/success*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start, document_end"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'\"}"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":"https://mail.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ndnaehgpjlnokgebbaldlmgkapkpjkkb/12.82.0_0/manifest.json"}]},{"group":"","name":"nlbgclfdbneidpjhchcipedhbffffghm","version":"1.0.1.7","description":"__MSG_ext_name__","purl":"pkg:chrome-extension/nlbgclfdbneidpjhchcipedhbffffghm@1.0.1.7","type":"application","bom-ref":"pkg:chrome-extension/nlbgclfdbneidpjhchcipedhbffffghm@1.0.1.7","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"http://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"unlimitedStorage"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nlbgclfdbneidpjhchcipedhbffffghm/1.0.1.7_0/manifest.json"}]},{"group":"","name":"nmmhkkegccagdldgiimedpiccmgmieda","version":"1.0.0.6","description":"__MSG_APP_NAME__","purl":"pkg:chrome-extension/nmmhkkegccagdldgiimedpiccmgmieda@1.0.0.6","type":"application","bom-ref":"pkg:chrome-extension/nmmhkkegccagdldgiimedpiccmgmieda@1.0.0.6","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"identity, webview, https://www.google.com/, https://www.googleapis.com/*, https://payments.google.com/payments/v4/js/integrator.js, https://sandbox.google.com/payments/v4/js/integrator.js"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"29"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nmmhkkegccagdldgiimedpiccmgmieda/1.0.0.6_0/manifest.json"}]},{"group":"","name":"oeopbcgkkoapgobdbedcemjljbihmemj","version":"36.2","description":"Checker Plus for Gmail™","purl":"pkg:chrome-extension/oeopbcgkkoapgobdbedcemjljbihmemj@36.2","type":"application","bom-ref":"pkg:chrome-extension/oeopbcgkkoapgobdbedcemjljbihmemj@36.2","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"gcm, alarms, activeTab, tts, idle, notifications, storage, contextMenus, identity, system.display, cookies, declarativeNetRequestWithHostAccess, offscreen, scripting, sidePanel"},{"name":"cdx:chrome-extension:optionalPermissions","value":"background, webRequest"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://mail.google.com/, https://mail.google.com/mail/mu/mp/*"},{"name":"cdx:chrome-extension:optionalHostPermissions","value":"https://*.googleusercontent.com/"},{"name":"cdx:chrome-extension:commands","value":"compose, dnd, markAllAsRead, markAsReadInNotificationWindow, openEmailDisplayedInNotificationWindow, quickComposeEmail, refresh"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"schema.json"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":"https://jasonsavard.com/*, https://checkout.stripe.com/*, https://www.paypal.com/*, https://nowpayments.io/*, https://mail.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/oeopbcgkkoapgobdbedcemjljbihmemj/36.2_0/manifest.json"}]},{"group":"","name":"ghbmnnjooekpmoecnnnilnnbdlolhkhi","version":"1.104.1","description":"__MSG_extName__","purl":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.104.1","type":"application","bom-ref":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.104.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Profile 1"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Profile 1"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"alarms, storage, unlimitedStorage, offscreen"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://docs.google.com/*, https://drive.google.com/*"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'\"}"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"dasherSettingSchema.json"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"88"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:externallyConnectableMatches","value":"https://docs.google.com/*, https://drive.google.com/*, https://drive-autopush.corp.google.com/*, https://drive-daily-0.corp.google.com/*, https://drive-daily-1.corp.google.com/*, https://drive-daily-2.corp.google.com/*, https://drive-daily-3.corp.google.com/*, https://drive-daily-4.corp.google.com/*, https://drive-daily-5.corp.google.com/*, https://drive-daily-6.corp.google.com/*, https://drive-preprod.corp.google.com/*, https://drive-staging.corp.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Profile 1/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.104.1_0/manifest.json"}]}],"services":[],"dependencies":[{"ref":"pkg:maven/com.example/sbom-example-app@1.0.0?type=jar","dependsOn":["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.16.0?type=jar","pkg:maven/com.h2database/h2@2.2.224?type=jar","pkg:maven/org.apache.commons/commons-lang3@3.14.0?type=jar","pkg:maven/org.projectlombok/lombok@1.18.30?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter-data-jpa@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter-test@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0?type=jar"]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0?type=jar","dependsOn":["pkg:maven/org.springframework.boot/spring-boot-starter-json@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter@3.2.0?type=jar","pkg:maven/org.springframework/spring-web@6.1.1?type=jar","pkg:maven/org.springframework/spring-webmvc@6.1.1?type=jar"]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter@3.2.0?type=jar","dependsOn":["pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar","pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot@3.2.0?type=jar","pkg:maven/org.yaml/snakeyaml@2.2?type=jar"]},{"ref":"pkg:maven/org.springframework.boot/spring-boot@3.2.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-autoconfigure@3.2.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-logging@3.2.0?type=jar","dependsOn":["pkg:maven/ch.qos.logback/logback-classic@1.4.11?type=jar","pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.21.1?type=jar","pkg:maven/org.slf4j/jul-to-slf4j@2.0.9?type=jar"]},{"ref":"pkg:maven/ch.qos.logback/logback-classic@1.4.11?type=jar","dependsOn":["pkg:maven/ch.qos.logback/logback-core@1.4.11?type=jar"]},{"ref":"pkg:maven/ch.qos.logback/logback-core@1.4.11?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.apache.logging.log4j/log4j-to-slf4j@2.21.1?type=jar","dependsOn":["pkg:maven/org.apache.logging.log4j/log4j-api@2.21.1?type=jar"]},{"ref":"pkg:maven/org.apache.logging.log4j/log4j-api@2.21.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.slf4j/jul-to-slf4j@2.0.9?type=jar","dependsOn":[]},{"ref":"pkg:maven/jakarta.annotation/jakarta.annotation-api@2.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.yaml/snakeyaml@2.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-json@3.2.0?type=jar","dependsOn":["pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.15.3?type=jar","pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.15.3?type=jar","pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.15.3?type=jar"]},{"ref":"pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.15.3?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.15.3?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.15.3?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-tomcat@3.2.0?type=jar","dependsOn":["pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.16?type=jar","pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.16?type=jar","pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.16?type=jar"]},{"ref":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@10.1.16?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-el@10.1.16?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.apache.tomcat.embed/tomcat-embed-websocket@10.1.16?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-web@6.1.1?type=jar","dependsOn":["pkg:maven/io.micrometer/micrometer-observation@1.12.0?type=jar","pkg:maven/org.springframework/spring-beans@6.1.1?type=jar"]},{"ref":"pkg:maven/org.springframework/spring-beans@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/io.micrometer/micrometer-observation@1.12.0?type=jar","dependsOn":["pkg:maven/io.micrometer/micrometer-commons@1.12.0?type=jar"]},{"ref":"pkg:maven/io.micrometer/micrometer-commons@1.12.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-webmvc@6.1.1?type=jar","dependsOn":["pkg:maven/org.springframework/spring-aop@6.1.1?type=jar","pkg:maven/org.springframework/spring-context@6.1.1?type=jar","pkg:maven/org.springframework/spring-expression@6.1.1?type=jar"]},{"ref":"pkg:maven/org.springframework/spring-aop@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-context@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-expression@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-data-jpa@3.2.0?type=jar","dependsOn":["pkg:maven/org.hibernate.orm/hibernate-core@6.3.1.Final?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter-aop@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot-starter-jdbc@3.2.0?type=jar","pkg:maven/org.springframework.data/spring-data-jpa@3.2.0?type=jar","pkg:maven/org.springframework/spring-aspects@6.1.1?type=jar"]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-aop@3.2.0?type=jar","dependsOn":["pkg:maven/org.aspectj/aspectjweaver@1.9.20.1?type=jar"]},{"ref":"pkg:maven/org.aspectj/aspectjweaver@1.9.20.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-jdbc@3.2.0?type=jar","dependsOn":["pkg:maven/com.zaxxer/HikariCP@5.0.1?type=jar","pkg:maven/org.springframework/spring-jdbc@6.1.1?type=jar"]},{"ref":"pkg:maven/com.zaxxer/HikariCP@5.0.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-jdbc@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.hibernate.orm/hibernate-core@6.3.1.Final?type=jar","dependsOn":["pkg:maven/com.fasterxml/classmate@1.5.1?type=jar","pkg:maven/io.smallrye/jandex@3.1.2?type=jar","pkg:maven/jakarta.inject/jakarta.inject-api@2.0.1?type=jar","pkg:maven/jakarta.persistence/jakarta.persistence-api@3.1.0?type=jar","pkg:maven/jakarta.transaction/jakarta.transaction-api@2.0.1?type=jar","pkg:maven/net.bytebuddy/byte-buddy@1.14.7?type=jar","pkg:maven/org.antlr/antlr4-runtime@4.10.1?type=jar","pkg:maven/org.glassfish.jaxb/jaxb-runtime@4.0.2?type=jar","pkg:maven/org.hibernate.common/hibernate-commons-annotations@6.0.6.Final?type=jar","pkg:maven/org.jboss.logging/jboss-logging@3.5.0.Final?type=jar"]},{"ref":"pkg:maven/jakarta.persistence/jakarta.persistence-api@3.1.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/jakarta.transaction/jakarta.transaction-api@2.0.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.jboss.logging/jboss-logging@3.5.0.Final?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.hibernate.common/hibernate-commons-annotations@6.0.6.Final?type=jar","dependsOn":[]},{"ref":"pkg:maven/io.smallrye/jandex@3.1.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.fasterxml/classmate@1.5.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/net.bytebuddy/byte-buddy@1.14.7?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.glassfish.jaxb/jaxb-runtime@4.0.2?type=jar","dependsOn":["pkg:maven/org.glassfish.jaxb/jaxb-core@4.0.2?type=jar"]},{"ref":"pkg:maven/org.glassfish.jaxb/jaxb-core@4.0.2?type=jar","dependsOn":["pkg:maven/com.sun.istack/istack-commons-runtime@4.1.1?type=jar","pkg:maven/org.eclipse.angus/angus-activation@2.0.0?type=jar","pkg:maven/org.glassfish.jaxb/txw2@4.0.2?type=jar"]},{"ref":"pkg:maven/org.eclipse.angus/angus-activation@2.0.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.glassfish.jaxb/txw2@4.0.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.sun.istack/istack-commons-runtime@4.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/jakarta.inject/jakarta.inject-api@2.0.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.antlr/antlr4-runtime@4.10.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.data/spring-data-jpa@3.2.0?type=jar","dependsOn":["pkg:maven/org.slf4j/slf4j-api@2.0.2?type=jar","pkg:maven/org.springframework.data/spring-data-commons@3.2.0?type=jar","pkg:maven/org.springframework/spring-orm@6.1.0?type=jar","pkg:maven/org.springframework/spring-tx@6.1.0?type=jar"]},{"ref":"pkg:maven/org.springframework.data/spring-data-commons@3.2.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-orm@6.1.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-tx@6.1.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.slf4j/slf4j-api@2.0.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-aspects@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.h2database/h2@2.2.224?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.projectlombok/lombok@1.18.30?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.apache.commons/commons-lang3@3.14.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.16.0?type=jar","dependsOn":["pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.16.0?type=jar","pkg:maven/com.fasterxml.jackson.core/jackson-core@2.16.0?type=jar"]},{"ref":"pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.16.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.fasterxml.jackson.core/jackson-core@2.16.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-starter-test@3.2.0?type=jar","dependsOn":["pkg:maven/com.jayway.jsonpath/json-path@2.8.0?type=jar","pkg:maven/jakarta.xml.bind/jakarta.xml.bind-api@4.0.1?type=jar","pkg:maven/net.minidev/json-smart@2.5.0?type=jar","pkg:maven/org.assertj/assertj-core@3.24.2?type=jar","pkg:maven/org.awaitility/awaitility@4.2.0?type=jar","pkg:maven/org.hamcrest/hamcrest@2.2?type=jar","pkg:maven/org.junit.jupiter/junit-jupiter@5.10.1?type=jar","pkg:maven/org.mockito/mockito-core@5.7.0?type=jar","pkg:maven/org.mockito/mockito-junit-jupiter@5.7.0?type=jar","pkg:maven/org.skyscreamer/jsonassert@1.5.1?type=jar","pkg:maven/org.springframework.boot/spring-boot-test-autoconfigure@3.2.0?type=jar","pkg:maven/org.springframework.boot/spring-boot-test@3.2.0?type=jar","pkg:maven/org.springframework/spring-core@6.1.1?type=jar","pkg:maven/org.springframework/spring-test@6.1.1?type=jar","pkg:maven/org.xmlunit/xmlunit-core@2.9.1?type=jar"]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-test@3.2.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework.boot/spring-boot-test-autoconfigure@3.2.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/com.jayway.jsonpath/json-path@2.8.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/jakarta.xml.bind/jakarta.xml.bind-api@4.0.1?type=jar","dependsOn":["pkg:maven/jakarta.activation/jakarta.activation-api@2.1.2?type=jar"]},{"ref":"pkg:maven/jakarta.activation/jakarta.activation-api@2.1.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/net.minidev/json-smart@2.5.0?type=jar","dependsOn":["pkg:maven/net.minidev/accessors-smart@2.5.0?type=jar"]},{"ref":"pkg:maven/net.minidev/accessors-smart@2.5.0?type=jar","dependsOn":["pkg:maven/org.ow2.asm/asm@9.3?type=jar"]},{"ref":"pkg:maven/org.ow2.asm/asm@9.3?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.assertj/assertj-core@3.24.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.awaitility/awaitility@4.2.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.hamcrest/hamcrest@2.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.junit.jupiter/junit-jupiter@5.10.1?type=jar","dependsOn":["pkg:maven/org.junit.jupiter/junit-jupiter-api@5.10.1?type=jar","pkg:maven/org.junit.jupiter/junit-jupiter-engine@5.10.1?type=jar","pkg:maven/org.junit.jupiter/junit-jupiter-params@5.10.1?type=jar"]},{"ref":"pkg:maven/org.junit.jupiter/junit-jupiter-api@5.10.1?type=jar","dependsOn":["pkg:maven/org.apiguardian/apiguardian-api@1.1.2?type=jar","pkg:maven/org.junit.platform/junit-platform-commons@1.10.1?type=jar","pkg:maven/org.opentest4j/opentest4j@1.3.0?type=jar"]},{"ref":"pkg:maven/org.opentest4j/opentest4j@1.3.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.junit.platform/junit-platform-commons@1.10.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.apiguardian/apiguardian-api@1.1.2?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.junit.jupiter/junit-jupiter-params@5.10.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.junit.jupiter/junit-jupiter-engine@5.10.1?type=jar","dependsOn":["pkg:maven/org.junit.platform/junit-platform-engine@1.10.1?type=jar"]},{"ref":"pkg:maven/org.junit.platform/junit-platform-engine@1.10.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.mockito/mockito-core@5.7.0?type=jar","dependsOn":["pkg:maven/net.bytebuddy/byte-buddy-agent@1.14.9?type=jar","pkg:maven/org.objenesis/objenesis@3.3?type=jar"]},{"ref":"pkg:maven/net.bytebuddy/byte-buddy-agent@1.14.9?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.objenesis/objenesis@3.3?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.mockito/mockito-junit-jupiter@5.7.0?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.skyscreamer/jsonassert@1.5.1?type=jar","dependsOn":["pkg:maven/com.vaadin.external.google/android-json@0.0.20131108.vaadin1?type=jar"]},{"ref":"pkg:maven/com.vaadin.external.google/android-json@0.0.20131108.vaadin1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-core@6.1.1?type=jar","dependsOn":["pkg:maven/org.springframework/spring-jcl@6.1.1?type=jar"]},{"ref":"pkg:maven/org.springframework/spring-jcl@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.springframework/spring-test@6.1.1?type=jar","dependsOn":[]},{"ref":"pkg:maven/org.xmlunit/xmlunit-core@2.9.1?type=jar","dependsOn":[]}]} \ No newline at end of file diff --git a/apps/backend/tests/fixtures/sbom/real_cyclonedx_node_dev.json b/apps/backend/tests/fixtures/sbom/real_cyclonedx_node_dev.json new file mode 100644 index 00000000..c8b9386c --- /dev/null +++ b/apps/backend/tests/fixtures/sbom/real_cyclonedx_node_dev.json @@ -0,0 +1 @@ +{"bomFormat":"CycloneDX","specVersion":"1.5","serialNumber":"urn:uuid:1eced03f-3ad0-4d63-9b53-7ed91f0863e0","version":1,"metadata":{"timestamp":"2026-07-10T15:01:59Z","tools":{"components":[{"group":"@cyclonedx","name":"cdxgen","version":"12.3.3","purl":"pkg:npm/%40cyclonedx/cdxgen@12.3.3","type":"application","bom-ref":"pkg:npm/@cyclonedx/cdxgen@12.3.3","author":"OWASP Foundation","publisher":"OWASP Foundation"}]},"authors":[{"name":"OWASP Foundation"}],"lifecycles":[{"phase":"build"}],"component":{"name":"sbom-example-nodejs","group":"","version":"1.0.0","description":"SBOM 생성 테스트를 위한 Express.js 예제 애플리케이션","purl":"pkg:npm/sbom-example-nodejs@1.0.0","bom-ref":"pkg:npm/sbom-example-nodejs@1.0.0","author":"SK Telecom","properties":[{"name":"cdx:npm:scripts","value":"start, dev, test, lint, format"}],"type":"application","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}]},"properties":[{"name":"cdx:bom:componentTypes","value":"chrome-extension\\nnpm"},{"name":"cdx:bom:componentNamespaces","value":"@babel\\n@bcoe\\n@colors\\n@dabh\\n@eslint\\n@eslint-community\\n@humanwhocodes\\n@istanbuljs\\n@jest\\n@jridgewell\\n@noble\\n@nodelib\\n@paralleldrive\\n@sinclair\\n@sinonjs\\n@so-ric\\n@types\\n@ungap"},{"name":"cdx:bom:componentSrcFiles","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/cfhdojbkjhnklbpkdaibdccddilifddb/4.41.2_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/egaejpfbkjamgheoingidhokbfnidlpi/1.6_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/emnphkkblegpebimobpbekeedfgemhof/1.6.10_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/fdpohaocaechififmbbbbbknoalclacl/8.6_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.107.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hahkjjkedonglpienpfiganogikkkoii/1.7_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hmjkmjkepdijhoojdojkdfohbdgmmhki/4.26091.600.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hoombieeljmmljlkjmnheibnpciblicm/5.1.8_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ijdefdijdmghafocfmmdojfghnpelnfn/3.33.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/inomeogfingihgjfjlpeplalcfajhgai/2.1_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/kobncfkmjelbefaoohoblamnbackjggk/1.0.30_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/lkcdojpmjehlniamnglpjlldkoonlomb/1.0.0.5_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ndnaehgpjlnokgebbaldlmgkapkpjkkb/12.82.0_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nlbgclfdbneidpjhchcipedhbffffghm/1.0.1.7_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nmmhkkegccagdldgiimedpiccmgmieda/1.0.0.6_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/oeopbcgkkoapgobdbedcemjljbihmemj/36.2_0/manifest.json\\n/Users/1112821/Library/Application Support/Google/Chrome/Profile 1/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.104.1_0/manifest.json\\npackage-lock.json"}]},"components":[{"group":"","name":"express","version":"4.22.2","scope":"required","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/express@4.22.2","type":"framework","bom-ref":"pkg:npm/express@4.22.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"express"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#8"}]}},{"group":"","name":"accepts","version":"1.3.8","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/accepts@1.3.8","type":"library","bom-ref":"pkg:npm/accepts@1.3.8","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"mime-types","version":"2.1.35","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/mime-types@2.1.35","type":"library","bom-ref":"pkg:npm/mime-types@2.1.35","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"mime-db","version":"1.52.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/mime-db@1.52.0","type":"library","bom-ref":"pkg:npm/mime-db@1.52.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"negotiator","version":"0.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/negotiator@0.6.3","type":"library","bom-ref":"pkg:npm/negotiator@0.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"array-flatten","version":"1.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/array-flatten@1.1.1","type":"library","bom-ref":"pkg:npm/array-flatten@1.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"body-parser","version":"1.20.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/body-parser@1.20.5","type":"library","bom-ref":"pkg:npm/body-parser@1.20.5","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"bytes","version":"3.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/bytes@3.1.2","type":"library","bom-ref":"pkg:npm/bytes@3.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"content-type","version":"1.0.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/content-type@1.0.5","type":"library","bom-ref":"pkg:npm/content-type@1.0.5","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"debug","version":"2.6.9","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/debug@2.6.9","type":"library","bom-ref":"pkg:npm/debug@2.6.9","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ms","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ms@2.0.0","type":"library","bom-ref":"pkg:npm/ms@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"depd","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/depd@2.0.0","type":"library","bom-ref":"pkg:npm/depd@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"destroy","version":"1.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/destroy@1.2.0","type":"library","bom-ref":"pkg:npm/destroy@1.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"http-errors","version":"2.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/http-errors@2.0.1","type":"library","bom-ref":"pkg:npm/http-errors@2.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"inherits","version":"2.0.4","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/inherits@2.0.4","type":"library","bom-ref":"pkg:npm/inherits@2.0.4","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"setprototypeof","version":"1.2.0","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/setprototypeof@1.2.0","type":"library","bom-ref":"pkg:npm/setprototypeof@1.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"statuses","version":"2.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/statuses@2.0.2","type":"library","bom-ref":"pkg:npm/statuses@2.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"toidentifier","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/toidentifier@1.0.1","type":"library","bom-ref":"pkg:npm/toidentifier@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"iconv-lite","version":"0.4.24","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/iconv-lite@0.4.24","type":"library","bom-ref":"pkg:npm/iconv-lite@0.4.24","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"safer-buffer","version":"2.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/safer-buffer@2.1.2","type":"library","bom-ref":"pkg:npm/safer-buffer@2.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"on-finished","version":"2.4.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/on-finished@2.4.1","type":"library","bom-ref":"pkg:npm/on-finished@2.4.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ee-first","version":"1.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ee-first@1.1.1","type":"library","bom-ref":"pkg:npm/ee-first@1.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"qs","version":"6.15.3","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/qs@6.15.3","type":"library","bom-ref":"pkg:npm/qs@6.15.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"es-define-property","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/es-define-property@1.0.1","type":"library","bom-ref":"pkg:npm/es-define-property@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"side-channel","version":"1.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/side-channel@1.1.1","type":"library","bom-ref":"pkg:npm/side-channel@1.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"es-errors","version":"1.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/es-errors@1.3.0","type":"library","bom-ref":"pkg:npm/es-errors@1.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"object-inspect","version":"1.13.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/object-inspect@1.13.4","type":"library","bom-ref":"pkg:npm/object-inspect@1.13.4","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"side-channel-list","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/side-channel-list@1.0.1","type":"library","bom-ref":"pkg:npm/side-channel-list@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"side-channel-map","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/side-channel-map@1.0.1","type":"library","bom-ref":"pkg:npm/side-channel-map@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"call-bound","version":"1.0.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/call-bound@1.0.4","type":"library","bom-ref":"pkg:npm/call-bound@1.0.4","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"call-bind-apply-helpers","version":"1.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/call-bind-apply-helpers@1.0.2","type":"library","bom-ref":"pkg:npm/call-bind-apply-helpers@1.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"function-bind","version":"1.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/function-bind@1.1.2","type":"library","bom-ref":"pkg:npm/function-bind@1.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"get-intrinsic","version":"1.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/get-intrinsic@1.3.0","type":"library","bom-ref":"pkg:npm/get-intrinsic@1.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"es-object-atoms","version":"1.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/es-object-atoms@1.1.2","type":"library","bom-ref":"pkg:npm/es-object-atoms@1.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"get-proto","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/get-proto@1.0.1","type":"library","bom-ref":"pkg:npm/get-proto@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"dunder-proto","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/dunder-proto@1.0.1","type":"library","bom-ref":"pkg:npm/dunder-proto@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"gopd","version":"1.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/gopd@1.2.0","type":"library","bom-ref":"pkg:npm/gopd@1.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"has-symbols","version":"1.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/has-symbols@1.1.0","type":"library","bom-ref":"pkg:npm/has-symbols@1.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"hasown","version":"2.0.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/hasown@2.0.4","type":"library","bom-ref":"pkg:npm/hasown@2.0.4","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"math-intrinsics","version":"1.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/math-intrinsics@1.1.0","type":"library","bom-ref":"pkg:npm/math-intrinsics@1.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"side-channel-weakmap","version":"1.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/side-channel-weakmap@1.0.2","type":"library","bom-ref":"pkg:npm/side-channel-weakmap@1.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"raw-body","version":"2.5.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/raw-body@2.5.3","type":"library","bom-ref":"pkg:npm/raw-body@2.5.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"unpipe","version":"1.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/unpipe@1.0.0","type":"library","bom-ref":"pkg:npm/unpipe@1.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"type-is","version":"1.6.18","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/type-is@1.6.18","type":"library","bom-ref":"pkg:npm/type-is@1.6.18","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"media-typer","version":"0.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/media-typer@0.3.0","type":"library","bom-ref":"pkg:npm/media-typer@0.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"content-disposition","version":"0.5.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/content-disposition@0.5.4","type":"library","bom-ref":"pkg:npm/content-disposition@0.5.4","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"safe-buffer","version":"5.2.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/safe-buffer@5.2.1","type":"library","bom-ref":"pkg:npm/safe-buffer@5.2.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cookie","version":"0.7.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/cookie@0.7.2","type":"library","bom-ref":"pkg:npm/cookie@0.7.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cookie-signature","version":"1.0.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/cookie-signature@1.0.7","type":"library","bom-ref":"pkg:npm/cookie-signature@1.0.7","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"encodeurl","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/encodeurl@2.0.0","type":"library","bom-ref":"pkg:npm/encodeurl@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"escape-html","version":"1.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/escape-html@1.0.3","type":"library","bom-ref":"pkg:npm/escape-html@1.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"etag","version":"1.8.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/etag@1.8.1","type":"library","bom-ref":"pkg:npm/etag@1.8.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"finalhandler","version":"1.3.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/finalhandler@1.3.2","type":"library","bom-ref":"pkg:npm/finalhandler@1.3.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"parseurl","version":"1.3.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/parseurl@1.3.3","type":"library","bom-ref":"pkg:npm/parseurl@1.3.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fresh","version":"0.5.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fresh@0.5.2","type":"library","bom-ref":"pkg:npm/fresh@0.5.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"merge-descriptors","version":"1.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/merge-descriptors@1.0.3","type":"library","bom-ref":"pkg:npm/merge-descriptors@1.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"methods","version":"1.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/methods@1.1.2","type":"library","bom-ref":"pkg:npm/methods@1.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"path-to-regexp","version":"0.1.13","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/path-to-regexp@0.1.13","type":"library","bom-ref":"pkg:npm/path-to-regexp@0.1.13","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"proxy-addr","version":"2.0.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/proxy-addr@2.0.7","type":"library","bom-ref":"pkg:npm/proxy-addr@2.0.7","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"forwarded","version":"0.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/forwarded@0.2.0","type":"library","bom-ref":"pkg:npm/forwarded@0.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ipaddr.js","version":"1.9.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ipaddr.js@1.9.1","type":"library","bom-ref":"pkg:npm/ipaddr.js@1.9.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"range-parser","version":"1.2.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/range-parser@1.2.1","type":"library","bom-ref":"pkg:npm/range-parser@1.2.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"send","version":"0.19.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/send@0.19.2","type":"library","bom-ref":"pkg:npm/send@0.19.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"mime","version":"1.6.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/mime@1.6.0","type":"library","bom-ref":"pkg:npm/mime@1.6.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ms","version":"2.1.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ms@2.1.3","type":"library","bom-ref":"pkg:npm/ms@2.1.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"serve-static","version":"1.16.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/serve-static@1.16.3","type":"library","bom-ref":"pkg:npm/serve-static@1.16.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"utils-merge","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/utils-merge@1.0.1","type":"library","bom-ref":"pkg:npm/utils-merge@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"vary","version":"1.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/vary@1.1.2","type":"library","bom-ref":"pkg:npm/vary@1.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cors","version":"2.8.6","scope":"required","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/cors@2.8.6","type":"library","bom-ref":"pkg:npm/cors@2.8.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"cors"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#9"}]}},{"group":"","name":"object-assign","version":"4.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/object-assign@4.1.1","type":"library","bom-ref":"pkg:npm/object-assign@4.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"helmet","version":"7.2.0","scope":"required","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/helmet@7.2.0","type":"library","bom-ref":"pkg:npm/helmet@7.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"helmet"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#10"}]}},{"group":"","name":"morgan","version":"1.11.0","scope":"required","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/morgan@1.11.0","type":"library","bom-ref":"pkg:npm/morgan@1.11.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"morgan"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#11"}]}},{"group":"","name":"basic-auth","version":"2.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/basic-auth@2.0.1","type":"library","bom-ref":"pkg:npm/basic-auth@2.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"safe-buffer","version":"5.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/safe-buffer@5.1.2","type":"library","bom-ref":"pkg:npm/safe-buffer@5.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"on-headers","version":"1.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/on-headers@1.1.0","type":"library","bom-ref":"pkg:npm/on-headers@1.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"dotenv","version":"16.6.1","scope":"required","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/dotenv@16.6.1","type":"library","bom-ref":"pkg:npm/dotenv@16.6.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"dotenv"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#15"}]}},{"group":"","name":"axios","version":"1.18.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/axios@1.18.1","type":"library","bom-ref":"pkg:npm/axios@1.18.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"follow-redirects","version":"1.16.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/follow-redirects@1.16.0","type":"library","bom-ref":"pkg:npm/follow-redirects@1.16.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"form-data","version":"4.0.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/form-data@4.0.6","type":"library","bom-ref":"pkg:npm/form-data@4.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"asynckit","version":"0.4.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/asynckit@0.4.0","type":"library","bom-ref":"pkg:npm/asynckit@0.4.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"combined-stream","version":"1.0.8","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/combined-stream@1.0.8","type":"library","bom-ref":"pkg:npm/combined-stream@1.0.8","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"delayed-stream","version":"1.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/delayed-stream@1.0.0","type":"library","bom-ref":"pkg:npm/delayed-stream@1.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"es-set-tostringtag","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/es-set-tostringtag@2.1.0","type":"library","bom-ref":"pkg:npm/es-set-tostringtag@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"has-tostringtag","version":"1.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/has-tostringtag@1.0.2","type":"library","bom-ref":"pkg:npm/has-tostringtag@1.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"https-proxy-agent","version":"5.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/https-proxy-agent@5.0.1","type":"library","bom-ref":"pkg:npm/https-proxy-agent@5.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"agent-base","version":"6.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/agent-base@6.0.2","type":"library","bom-ref":"pkg:npm/agent-base@6.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"debug","version":"4.4.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/debug@4.4.3","type":"library","bom-ref":"pkg:npm/debug@4.4.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"proxy-from-env","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/proxy-from-env@2.1.0","type":"library","bom-ref":"pkg:npm/proxy-from-env@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"lodash","version":"4.18.1","scope":"required","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/lodash@4.18.1","type":"library","bom-ref":"pkg:npm/lodash@4.18.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"lodash"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#13"}]}},{"group":"","name":"moment","version":"2.30.1","scope":"required","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/moment@2.30.1","type":"library","bom-ref":"pkg:npm/moment@2.30.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"moment"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#14"}]}},{"group":"","name":"winston","version":"3.19.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/winston@3.19.0","type":"library","bom-ref":"pkg:npm/winston@3.19.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@colors","name":"colors","version":"1.6.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40colors/colors@1.6.0","type":"library","bom-ref":"pkg:npm/@colors/colors@1.6.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@dabh","name":"diagnostics","version":"2.0.8","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40dabh/diagnostics@2.0.8","type":"library","bom-ref":"pkg:npm/@dabh/diagnostics@2.0.8","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@so-ric","name":"colorspace","version":"1.1.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40so-ric/colorspace@1.1.6","type":"library","bom-ref":"pkg:npm/@so-ric/colorspace@1.1.6","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"color","version":"5.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/color@5.0.3","type":"library","bom-ref":"pkg:npm/color@5.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"color-convert","version":"3.1.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/color-convert@3.1.3","type":"library","bom-ref":"pkg:npm/color-convert@3.1.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"color-name","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/color-name@2.1.0","type":"library","bom-ref":"pkg:npm/color-name@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"color-string","version":"2.1.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/color-string@2.1.4","type":"library","bom-ref":"pkg:npm/color-string@2.1.4","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"text-hex","version":"1.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/text-hex@1.0.0","type":"library","bom-ref":"pkg:npm/text-hex@1.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"enabled","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/enabled@2.0.0","type":"library","bom-ref":"pkg:npm/enabled@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"kuler","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/kuler@2.0.0","type":"library","bom-ref":"pkg:npm/kuler@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"async","version":"3.2.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/async@3.2.6","type":"library","bom-ref":"pkg:npm/async@3.2.6","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-stream","version":"2.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-stream@2.0.1","type":"library","bom-ref":"pkg:npm/is-stream@2.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"logform","version":"2.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/logform@2.7.0","type":"library","bom-ref":"pkg:npm/logform@2.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"triple-beam","version":"1.3.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/triple-beam@1.3.5","type":"library","bom-ref":"pkg:npm/@types/triple-beam@1.3.5","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fecha","version":"4.2.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fecha@4.2.3","type":"library","bom-ref":"pkg:npm/fecha@4.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"safe-stable-stringify","version":"2.5.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/safe-stable-stringify@2.5.0","type":"library","bom-ref":"pkg:npm/safe-stable-stringify@2.5.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"triple-beam","version":"1.4.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/triple-beam@1.4.1","type":"library","bom-ref":"pkg:npm/triple-beam@1.4.1","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"one-time","version":"1.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/one-time@1.0.0","type":"library","bom-ref":"pkg:npm/one-time@1.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fn.name","version":"1.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fn.name@1.1.0","type":"library","bom-ref":"pkg:npm/fn.name@1.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"readable-stream","version":"3.6.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/readable-stream@3.6.2","type":"library","bom-ref":"pkg:npm/readable-stream@3.6.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"string_decoder","version":"1.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/string_decoder@1.3.0","type":"library","bom-ref":"pkg:npm/string_decoder@1.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"util-deprecate","version":"1.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/util-deprecate@1.0.2","type":"library","bom-ref":"pkg:npm/util-deprecate@1.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"stack-trace","version":"0.0.10","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/stack-trace@0.0.10","type":"library","bom-ref":"pkg:npm/stack-trace@0.0.10","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"winston-transport","version":"4.9.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/winston-transport@4.9.0","type":"library","bom-ref":"pkg:npm/winston-transport@4.9.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"compression","version":"1.8.1","scope":"required","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/compression@1.8.1","type":"library","bom-ref":"pkg:npm/compression@1.8.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"ImportedModules","value":"compression"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]},"occurrences":[{"location":"index.js#12"}]}},{"group":"","name":"compressible","version":"2.0.18","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/compressible@2.0.18","type":"library","bom-ref":"pkg:npm/compressible@2.0.18","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"mime-db","version":"1.54.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/mime-db@1.54.0","type":"library","bom-ref":"pkg:npm/mime-db@1.54.0","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"negotiator","version":"0.6.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/negotiator@0.6.4","type":"library","bom-ref":"pkg:npm/negotiator@0.6.4","properties":[{"name":"SrcFile","value":"package-lock.json"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"nodemon","version":"3.1.14","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/nodemon@3.1.14","type":"library","bom-ref":"pkg:npm/nodemon@3.1.14","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"chokidar","version":"3.6.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/chokidar@3.6.0","type":"library","bom-ref":"pkg:npm/chokidar@3.6.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"anymatch","version":"3.1.3","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/anymatch@3.1.3","type":"library","bom-ref":"pkg:npm/anymatch@3.1.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"normalize-path","version":"3.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/normalize-path@3.0.0","type":"library","bom-ref":"pkg:npm/normalize-path@3.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"picomatch","version":"2.3.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/picomatch@2.3.2","type":"library","bom-ref":"pkg:npm/picomatch@2.3.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"braces","version":"3.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/braces@3.0.3","type":"library","bom-ref":"pkg:npm/braces@3.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fill-range","version":"7.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fill-range@7.1.1","type":"library","bom-ref":"pkg:npm/fill-range@7.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"to-regex-range","version":"5.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/to-regex-range@5.0.1","type":"library","bom-ref":"pkg:npm/to-regex-range@5.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-number","version":"7.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-number@7.0.0","type":"library","bom-ref":"pkg:npm/is-number@7.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"glob-parent","version":"5.1.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/glob-parent@5.1.2","type":"library","bom-ref":"pkg:npm/glob-parent@5.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-glob","version":"4.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-glob@4.0.3","type":"library","bom-ref":"pkg:npm/is-glob@4.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-extglob","version":"2.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-extglob@2.1.1","type":"library","bom-ref":"pkg:npm/is-extglob@2.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-binary-path","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-binary-path@2.1.0","type":"library","bom-ref":"pkg:npm/is-binary-path@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"binary-extensions","version":"2.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/binary-extensions@2.3.0","type":"library","bom-ref":"pkg:npm/binary-extensions@2.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"readdirp","version":"3.6.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/readdirp@3.6.0","type":"library","bom-ref":"pkg:npm/readdirp@3.6.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fsevents","version":"2.3.3","scope":"optional","hashes":[{"alg":"SHA-512","content":"e71a037d7f9f2fb7da0139da82658fa5b16dc21fd1efb5a630caaa1c64bae42defbc1d181eb805f81d58999df8e35b4c8f99fade4d36d765cda09c339617df43"}],"licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fsevents@2.3.3","externalReferences":[{"type":"distribution","url":"https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"}],"type":"library","bom-ref":"pkg:npm/fsevents@2.3.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"},{"name":"ResolvedUrl","value":"https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"},{"name":"cdx:npm:hasInstallScript","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ignore-by-default","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/ignore-by-default@1.0.1","type":"library","bom-ref":"pkg:npm/ignore-by-default@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"minimatch","version":"10.2.5","scope":"optional","licenses":[{"license":{"id":"BlueOak-1.0.0","url":"https://opensource.org/licenses/BlueOak-1.0.0"}}],"purl":"pkg:npm/minimatch@10.2.5","type":"library","bom-ref":"pkg:npm/minimatch@10.2.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"brace-expansion","version":"5.0.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/brace-expansion@5.0.6","type":"library","bom-ref":"pkg:npm/brace-expansion@5.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"balanced-match","version":"4.0.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/balanced-match@4.0.4","type":"library","bom-ref":"pkg:npm/balanced-match@4.0.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"pstree.remy","version":"1.1.8","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/pstree.remy@1.1.8","type":"library","bom-ref":"pkg:npm/pstree.remy@1.1.8","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"semver","version":"7.8.5","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/semver@7.8.5","type":"library","bom-ref":"pkg:npm/semver@7.8.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"simple-update-notifier","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/simple-update-notifier@2.0.0","type":"library","bom-ref":"pkg:npm/simple-update-notifier@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"supports-color","version":"5.5.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/supports-color@5.5.0","type":"library","bom-ref":"pkg:npm/supports-color@5.5.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"has-flag","version":"3.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/has-flag@3.0.0","type":"library","bom-ref":"pkg:npm/has-flag@3.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"touch","version":"3.1.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/touch@3.1.1","type":"library","bom-ref":"pkg:npm/touch@3.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"undefsafe","version":"2.0.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/undefsafe@2.0.5","type":"library","bom-ref":"pkg:npm/undefsafe@2.0.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest@29.7.0","type":"library","bom-ref":"pkg:npm/jest@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"core","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/core@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/core@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"console","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/console@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/console@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"types","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/types@29.6.3","type":"library","bom-ref":"pkg:npm/@jest/types@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"schemas","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/schemas@29.6.3","type":"library","bom-ref":"pkg:npm/@jest/schemas@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@sinclair","name":"typebox","version":"0.27.10","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40sinclair/typebox@0.27.10","type":"library","bom-ref":"pkg:npm/@sinclair/typebox@0.27.10","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"istanbul-lib-coverage","version":"2.0.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/istanbul-lib-coverage@2.0.6","type":"library","bom-ref":"pkg:npm/@types/istanbul-lib-coverage@2.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"istanbul-reports","version":"3.0.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/istanbul-reports@3.0.4","type":"library","bom-ref":"pkg:npm/@types/istanbul-reports@3.0.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"istanbul-lib-report","version":"3.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/istanbul-lib-report@3.0.3","type":"library","bom-ref":"pkg:npm/@types/istanbul-lib-report@3.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"node","version":"26.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/node@26.0.1","type":"library","bom-ref":"pkg:npm/@types/node@26.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"undici-types","version":"8.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/undici-types@8.3.0","type":"library","bom-ref":"pkg:npm/undici-types@8.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"yargs","version":"17.0.35","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/yargs@17.0.35","type":"library","bom-ref":"pkg:npm/@types/yargs@17.0.35","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"yargs-parser","version":"21.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/yargs-parser@21.0.3","type":"library","bom-ref":"pkg:npm/@types/yargs-parser@21.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"chalk","version":"4.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/chalk@4.1.2","type":"library","bom-ref":"pkg:npm/chalk@4.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ansi-styles","version":"4.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ansi-styles@4.3.0","type":"library","bom-ref":"pkg:npm/ansi-styles@4.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"color-convert","version":"2.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/color-convert@2.0.1","type":"library","bom-ref":"pkg:npm/color-convert@2.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"color-name","version":"1.1.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/color-name@1.1.4","type":"library","bom-ref":"pkg:npm/color-name@1.1.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"supports-color","version":"7.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/supports-color@7.2.0","type":"library","bom-ref":"pkg:npm/supports-color@7.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"has-flag","version":"4.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/has-flag@4.0.0","type":"library","bom-ref":"pkg:npm/has-flag@4.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-message-util","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-message-util@29.7.0","type":"library","bom-ref":"pkg:npm/jest-message-util@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"code-frame","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/code-frame@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/code-frame@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-validator-identifier","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-validator-identifier@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-validator-identifier@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"js-tokens","version":"4.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/js-tokens@4.0.0","type":"library","bom-ref":"pkg:npm/js-tokens@4.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"picocolors","version":"1.1.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/picocolors@1.1.1","type":"library","bom-ref":"pkg:npm/picocolors@1.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"stack-utils","version":"2.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/stack-utils@2.0.3","type":"library","bom-ref":"pkg:npm/@types/stack-utils@2.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"graceful-fs","version":"4.2.11","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/graceful-fs@4.2.11","type":"library","bom-ref":"pkg:npm/graceful-fs@4.2.11","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"micromatch","version":"4.0.8","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/micromatch@4.0.8","type":"library","bom-ref":"pkg:npm/micromatch@4.0.8","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"pretty-format","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/pretty-format@29.7.0","type":"library","bom-ref":"pkg:npm/pretty-format@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ansi-styles","version":"5.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ansi-styles@5.2.0","type":"library","bom-ref":"pkg:npm/ansi-styles@5.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"react-is","version":"18.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/react-is@18.3.1","type":"framework","bom-ref":"pkg:npm/react-is@18.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"slash","version":"3.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/slash@3.0.0","type":"library","bom-ref":"pkg:npm/slash@3.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"stack-utils","version":"2.0.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/stack-utils@2.0.6","type":"library","bom-ref":"pkg:npm/stack-utils@2.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"escape-string-regexp","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/escape-string-regexp@2.0.0","type":"library","bom-ref":"pkg:npm/escape-string-regexp@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-util","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-util@29.7.0","type":"library","bom-ref":"pkg:npm/jest-util@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ci-info","version":"3.9.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ci-info@3.9.0","type":"library","bom-ref":"pkg:npm/ci-info@3.9.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"reporters","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/reporters@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/reporters@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@bcoe","name":"v8-coverage","version":"0.2.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40bcoe/v8-coverage@0.2.3","type":"library","bom-ref":"pkg:npm/@bcoe/v8-coverage@0.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"test-result","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/test-result@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/test-result@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"collect-v8-coverage","version":"1.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/collect-v8-coverage@1.0.3","type":"library","bom-ref":"pkg:npm/collect-v8-coverage@1.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"transform","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/transform@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/transform@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"core","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/core@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/core@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"generator","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/generator@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/generator@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"parser","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/parser@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/parser@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"types","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/types@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/types@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-string-parser","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-string-parser@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-string-parser@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jridgewell","name":"gen-mapping","version":"0.3.13","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jridgewell/gen-mapping@0.3.13","type":"library","bom-ref":"pkg:npm/@jridgewell/gen-mapping@0.3.13","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jridgewell","name":"sourcemap-codec","version":"1.5.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jridgewell/sourcemap-codec@1.5.5","type":"library","bom-ref":"pkg:npm/@jridgewell/sourcemap-codec@1.5.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jridgewell","name":"trace-mapping","version":"0.3.31","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jridgewell/trace-mapping@0.3.31","type":"library","bom-ref":"pkg:npm/@jridgewell/trace-mapping@0.3.31","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jridgewell","name":"resolve-uri","version":"3.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jridgewell/resolve-uri@3.1.2","type":"library","bom-ref":"pkg:npm/@jridgewell/resolve-uri@3.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jsesc","version":"3.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jsesc@3.1.0","type":"library","bom-ref":"pkg:npm/jsesc@3.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-compilation-targets","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-compilation-targets@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-compilation-targets@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"compat-data","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/compat-data@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/compat-data@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-validator-option","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-validator-option@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-validator-option@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"browserslist","version":"4.28.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/browserslist@4.28.4","type":"library","bom-ref":"pkg:npm/browserslist@4.28.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"baseline-browser-mapping","version":"2.10.40","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/baseline-browser-mapping@2.10.40","type":"library","bom-ref":"pkg:npm/baseline-browser-mapping@2.10.40","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"caniuse-lite","version":"1.0.30001799","scope":"optional","licenses":[{"license":{"id":"CC-BY-4.0","url":"https://opensource.org/licenses/CC-BY-4.0"}}],"purl":"pkg:npm/caniuse-lite@1.0.30001799","type":"library","bom-ref":"pkg:npm/caniuse-lite@1.0.30001799","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"electron-to-chromium","version":"1.5.380","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/electron-to-chromium@1.5.380","type":"library","bom-ref":"pkg:npm/electron-to-chromium@1.5.380","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"node-releases","version":"2.0.50","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/node-releases@2.0.50","type":"library","bom-ref":"pkg:npm/node-releases@2.0.50","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"update-browserslist-db","version":"1.2.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/update-browserslist-db@1.2.3","type":"library","bom-ref":"pkg:npm/update-browserslist-db@1.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"escalade","version":"3.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/escalade@3.2.0","type":"library","bom-ref":"pkg:npm/escalade@3.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"lru-cache","version":"5.1.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/lru-cache@5.1.1","type":"library","bom-ref":"pkg:npm/lru-cache@5.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"yallist","version":"3.1.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/yallist@3.1.1","type":"library","bom-ref":"pkg:npm/yallist@3.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"semver","version":"6.3.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/semver@6.3.1","type":"library","bom-ref":"pkg:npm/semver@6.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-module-transforms","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-module-transforms@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-module-transforms@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-module-imports","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-module-imports@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-module-imports@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"traverse","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/traverse@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/traverse@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-globals","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-globals@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-globals@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"template","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/template@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/template@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helpers","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helpers@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helpers@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jridgewell","name":"remapping","version":"2.3.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jridgewell/remapping@2.3.5","type":"library","bom-ref":"pkg:npm/@jridgewell/remapping@2.3.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"convert-source-map","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/convert-source-map@2.0.0","type":"library","bom-ref":"pkg:npm/convert-source-map@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"gensync","version":"1.0.0-beta.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/gensync@1.0.0-beta.2","type":"library","bom-ref":"pkg:npm/gensync@1.0.0-beta.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"json5","version":"2.2.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/json5@2.2.3","type":"library","bom-ref":"pkg:npm/json5@2.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"babel-plugin-istanbul","version":"6.1.1","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/babel-plugin-istanbul@6.1.1","type":"library","bom-ref":"pkg:npm/babel-plugin-istanbul@6.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"helper-plugin-utils","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/helper-plugin-utils@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/helper-plugin-utils@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@istanbuljs","name":"load-nyc-config","version":"1.1.0","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/%40istanbuljs/load-nyc-config@1.1.0","type":"library","bom-ref":"pkg:npm/@istanbuljs/load-nyc-config@1.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"camelcase","version":"5.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/camelcase@5.3.1","type":"library","bom-ref":"pkg:npm/camelcase@5.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"find-up","version":"4.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/find-up@4.1.0","type":"library","bom-ref":"pkg:npm/find-up@4.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"locate-path","version":"5.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/locate-path@5.0.0","type":"library","bom-ref":"pkg:npm/locate-path@5.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"p-locate","version":"4.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/p-locate@4.1.0","type":"library","bom-ref":"pkg:npm/p-locate@4.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"p-limit","version":"2.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/p-limit@2.3.0","type":"library","bom-ref":"pkg:npm/p-limit@2.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"p-try","version":"2.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/p-try@2.2.0","type":"library","bom-ref":"pkg:npm/p-try@2.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"path-exists","version":"4.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/path-exists@4.0.0","type":"library","bom-ref":"pkg:npm/path-exists@4.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"get-package-type","version":"0.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/get-package-type@0.1.0","type":"library","bom-ref":"pkg:npm/get-package-type@0.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"js-yaml","version":"3.15.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/js-yaml@3.15.0","type":"library","bom-ref":"pkg:npm/js-yaml@3.15.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"argparse","version":"1.0.10","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/argparse@1.0.10","type":"library","bom-ref":"pkg:npm/argparse@1.0.10","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"sprintf-js","version":"1.0.3","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/sprintf-js@1.0.3","type":"library","bom-ref":"pkg:npm/sprintf-js@1.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"esprima","version":"4.0.1","scope":"optional","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/esprima@4.0.1","type":"library","bom-ref":"pkg:npm/esprima@4.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"resolve-from","version":"5.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/resolve-from@5.0.0","type":"library","bom-ref":"pkg:npm/resolve-from@5.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@istanbuljs","name":"schema","version":"0.1.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40istanbuljs/schema@0.1.6","type":"library","bom-ref":"pkg:npm/@istanbuljs/schema@0.1.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"istanbul-lib-instrument","version":"5.2.1","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/istanbul-lib-instrument@5.2.1","type":"library","bom-ref":"pkg:npm/istanbul-lib-instrument@5.2.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"istanbul-lib-coverage","version":"3.2.2","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/istanbul-lib-coverage@3.2.2","type":"library","bom-ref":"pkg:npm/istanbul-lib-coverage@3.2.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"test-exclude","version":"6.0.0","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/test-exclude@6.0.0","type":"library","bom-ref":"pkg:npm/test-exclude@6.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"glob","version":"7.2.3","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/glob@7.2.3","type":"library","bom-ref":"pkg:npm/glob@7.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fs.realpath","version":"1.0.0","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/fs.realpath@1.0.0","type":"library","bom-ref":"pkg:npm/fs.realpath@1.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"inflight","version":"1.0.6","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/inflight@1.0.6","type":"library","bom-ref":"pkg:npm/inflight@1.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"once","version":"1.4.0","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/once@1.4.0","type":"library","bom-ref":"pkg:npm/once@1.4.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"wrappy","version":"1.0.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/wrappy@1.0.2","type":"library","bom-ref":"pkg:npm/wrappy@1.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"minimatch","version":"3.1.5","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/minimatch@3.1.5","type":"library","bom-ref":"pkg:npm/minimatch@3.1.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"brace-expansion","version":"1.1.15","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/brace-expansion@1.1.15","type":"library","bom-ref":"pkg:npm/brace-expansion@1.1.15","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"balanced-match","version":"1.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/balanced-match@1.0.2","type":"library","bom-ref":"pkg:npm/balanced-match@1.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"concat-map","version":"0.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/concat-map@0.0.1","type":"library","bom-ref":"pkg:npm/concat-map@0.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"path-is-absolute","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/path-is-absolute@1.0.1","type":"library","bom-ref":"pkg:npm/path-is-absolute@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fast-json-stable-stringify","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fast-json-stable-stringify@2.1.0","type":"library","bom-ref":"pkg:npm/fast-json-stable-stringify@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-haste-map","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-haste-map@29.7.0","type":"library","bom-ref":"pkg:npm/jest-haste-map@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"graceful-fs","version":"4.1.9","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/graceful-fs@4.1.9","type":"library","bom-ref":"pkg:npm/@types/graceful-fs@4.1.9","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fb-watchman","version":"2.0.2","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/fb-watchman@2.0.2","type":"library","bom-ref":"pkg:npm/fb-watchman@2.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"bser","version":"2.1.1","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/bser@2.1.1","type":"library","bom-ref":"pkg:npm/bser@2.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"node-int64","version":"0.4.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/node-int64@0.4.0","type":"library","bom-ref":"pkg:npm/node-int64@0.4.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-regex-util","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-regex-util@29.6.3","type":"library","bom-ref":"pkg:npm/jest-regex-util@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-worker","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-worker@29.7.0","type":"library","bom-ref":"pkg:npm/jest-worker@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"merge-stream","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/merge-stream@2.0.0","type":"library","bom-ref":"pkg:npm/merge-stream@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"supports-color","version":"8.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/supports-color@8.1.1","type":"library","bom-ref":"pkg:npm/supports-color@8.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"walker","version":"1.0.8","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/walker@1.0.8","type":"library","bom-ref":"pkg:npm/walker@1.0.8","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"makeerror","version":"1.0.12","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/makeerror@1.0.12","type":"library","bom-ref":"pkg:npm/makeerror@1.0.12","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"tmpl","version":"1.0.5","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/tmpl@1.0.5","type":"library","bom-ref":"pkg:npm/tmpl@1.0.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"pirates","version":"4.0.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/pirates@4.0.7","type":"library","bom-ref":"pkg:npm/pirates@4.0.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"write-file-atomic","version":"4.0.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/write-file-atomic@4.0.2","type":"library","bom-ref":"pkg:npm/write-file-atomic@4.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"imurmurhash","version":"0.1.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/imurmurhash@0.1.4","type":"library","bom-ref":"pkg:npm/imurmurhash@0.1.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"signal-exit","version":"3.0.7","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/signal-exit@3.0.7","type":"library","bom-ref":"pkg:npm/signal-exit@3.0.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"exit","version":"0.1.2","scope":"optional","purl":"pkg:npm/exit@0.1.2","type":"library","bom-ref":"pkg:npm/exit@0.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"istanbul-lib-instrument","version":"6.0.3","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/istanbul-lib-instrument@6.0.3","type":"library","bom-ref":"pkg:npm/istanbul-lib-instrument@6.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"istanbul-lib-report","version":"3.0.1","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/istanbul-lib-report@3.0.1","type":"library","bom-ref":"pkg:npm/istanbul-lib-report@3.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"make-dir","version":"4.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/make-dir@4.0.0","type":"library","bom-ref":"pkg:npm/make-dir@4.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"istanbul-lib-source-maps","version":"4.0.1","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/istanbul-lib-source-maps@4.0.1","type":"library","bom-ref":"pkg:npm/istanbul-lib-source-maps@4.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"source-map","version":"0.6.1","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/source-map@0.6.1","type":"library","bom-ref":"pkg:npm/source-map@0.6.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"istanbul-reports","version":"3.2.0","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/istanbul-reports@3.2.0","type":"library","bom-ref":"pkg:npm/istanbul-reports@3.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"html-escaper","version":"2.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/html-escaper@2.0.2","type":"library","bom-ref":"pkg:npm/html-escaper@2.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"string-length","version":"4.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/string-length@4.0.2","type":"library","bom-ref":"pkg:npm/string-length@4.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"char-regex","version":"1.0.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/char-regex@1.0.2","type":"library","bom-ref":"pkg:npm/char-regex@1.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"strip-ansi","version":"6.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/strip-ansi@6.0.1","type":"library","bom-ref":"pkg:npm/strip-ansi@6.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ansi-regex","version":"5.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ansi-regex@5.0.1","type":"library","bom-ref":"pkg:npm/ansi-regex@5.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"v8-to-istanbul","version":"9.3.0","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/v8-to-istanbul@9.3.0","type":"library","bom-ref":"pkg:npm/v8-to-istanbul@9.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ansi-escapes","version":"4.3.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ansi-escapes@4.3.2","type":"library","bom-ref":"pkg:npm/ansi-escapes@4.3.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"type-fest","version":"0.21.3","scope":"optional","licenses":[{"expression":"(MIT OR CC0-1.0)"}],"purl":"pkg:npm/type-fest@0.21.3","type":"library","bom-ref":"pkg:npm/type-fest@0.21.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-changed-files","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-changed-files@29.7.0","type":"library","bom-ref":"pkg:npm/jest-changed-files@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"execa","version":"5.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/execa@5.1.1","type":"library","bom-ref":"pkg:npm/execa@5.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cross-spawn","version":"7.0.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/cross-spawn@7.0.6","type":"library","bom-ref":"pkg:npm/cross-spawn@7.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"path-key","version":"3.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/path-key@3.1.1","type":"library","bom-ref":"pkg:npm/path-key@3.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"shebang-command","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/shebang-command@2.0.0","type":"library","bom-ref":"pkg:npm/shebang-command@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"shebang-regex","version":"3.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/shebang-regex@3.0.0","type":"library","bom-ref":"pkg:npm/shebang-regex@3.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"which","version":"2.0.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/which@2.0.2","type":"library","bom-ref":"pkg:npm/which@2.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"isexe","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/isexe@2.0.0","type":"library","bom-ref":"pkg:npm/isexe@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"get-stream","version":"6.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/get-stream@6.0.1","type":"library","bom-ref":"pkg:npm/get-stream@6.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"human-signals","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/human-signals@2.1.0","type":"library","bom-ref":"pkg:npm/human-signals@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"npm-run-path","version":"4.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/npm-run-path@4.0.1","type":"library","bom-ref":"pkg:npm/npm-run-path@4.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"onetime","version":"5.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/onetime@5.1.2","type":"library","bom-ref":"pkg:npm/onetime@5.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"mimic-fn","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/mimic-fn@2.1.0","type":"library","bom-ref":"pkg:npm/mimic-fn@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"strip-final-newline","version":"2.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/strip-final-newline@2.0.0","type":"library","bom-ref":"pkg:npm/strip-final-newline@2.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"p-limit","version":"3.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/p-limit@3.1.0","type":"library","bom-ref":"pkg:npm/p-limit@3.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"yocto-queue","version":"0.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/yocto-queue@0.1.0","type":"library","bom-ref":"pkg:npm/yocto-queue@0.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-config","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-config@29.7.0","type":"library","bom-ref":"pkg:npm/jest-config@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"test-sequencer","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/test-sequencer@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/test-sequencer@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"babel-jest","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/babel-jest@29.7.0","type":"library","bom-ref":"pkg:npm/babel-jest@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"babel__core","version":"7.20.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/babel__core@7.20.5","type":"library","bom-ref":"pkg:npm/@types/babel__core@7.20.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"babel__generator","version":"7.27.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/babel__generator@7.27.0","type":"library","bom-ref":"pkg:npm/@types/babel__generator@7.27.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"babel__template","version":"7.4.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/babel__template@7.4.4","type":"library","bom-ref":"pkg:npm/@types/babel__template@7.4.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@types","name":"babel__traverse","version":"7.28.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40types/babel__traverse@7.28.0","type":"library","bom-ref":"pkg:npm/@types/babel__traverse@7.28.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"babel-preset-jest","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/babel-preset-jest@29.6.3","type":"library","bom-ref":"pkg:npm/babel-preset-jest@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"babel-plugin-jest-hoist","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/babel-plugin-jest-hoist@29.6.3","type":"library","bom-ref":"pkg:npm/babel-plugin-jest-hoist@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"babel-preset-current-node-syntax","version":"1.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/babel-preset-current-node-syntax@1.2.0","type":"library","bom-ref":"pkg:npm/babel-preset-current-node-syntax@1.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-async-generators","version":"7.8.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-async-generators@7.8.4","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-async-generators@7.8.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-bigint","version":"7.8.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-bigint@7.8.3","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-bigint@7.8.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-class-properties","version":"7.12.13","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-class-properties@7.12.13","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-class-properties@7.12.13","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-class-static-block","version":"7.14.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-class-static-block@7.14.5","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-class-static-block@7.14.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-import-attributes","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-import-attributes@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-import-attributes@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-import-meta","version":"7.10.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-import-meta@7.10.4","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-import-meta@7.10.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-json-strings","version":"7.8.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-json-strings@7.8.3","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-json-strings@7.8.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-logical-assignment-operators","version":"7.10.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-logical-assignment-operators@7.10.4","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-logical-assignment-operators@7.10.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-nullish-coalescing-operator","version":"7.8.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-nullish-coalescing-operator@7.8.3","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-nullish-coalescing-operator@7.8.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-numeric-separator","version":"7.10.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-numeric-separator@7.10.4","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-numeric-separator@7.10.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-object-rest-spread","version":"7.8.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-object-rest-spread@7.8.3","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-object-rest-spread@7.8.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-optional-catch-binding","version":"7.8.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-optional-catch-binding@7.8.3","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-optional-catch-binding@7.8.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-optional-chaining","version":"7.8.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-optional-chaining@7.8.3","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-optional-chaining@7.8.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-private-property-in-object","version":"7.14.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-private-property-in-object@7.14.5","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-private-property-in-object@7.14.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-top-level-await","version":"7.14.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-top-level-await@7.14.5","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-top-level-await@7.14.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"deepmerge","version":"4.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/deepmerge@4.3.1","type":"library","bom-ref":"pkg:npm/deepmerge@4.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-circus","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-circus@29.7.0","type":"library","bom-ref":"pkg:npm/jest-circus@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"environment","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/environment@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/environment@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"fake-timers","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/fake-timers@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/fake-timers@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@sinonjs","name":"fake-timers","version":"10.3.0","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/%40sinonjs/fake-timers@10.3.0","type":"library","bom-ref":"pkg:npm/@sinonjs/fake-timers@10.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@sinonjs","name":"commons","version":"3.0.1","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/%40sinonjs/commons@3.0.1","type":"library","bom-ref":"pkg:npm/@sinonjs/commons@3.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"type-detect","version":"4.0.8","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/type-detect@4.0.8","type":"library","bom-ref":"pkg:npm/type-detect@4.0.8","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-mock","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-mock@29.7.0","type":"library","bom-ref":"pkg:npm/jest-mock@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"expect","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/expect@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/expect@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"expect","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/expect@29.7.0","type":"library","bom-ref":"pkg:npm/expect@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"expect-utils","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/expect-utils@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/expect-utils@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-get-type","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-get-type@29.6.3","type":"library","bom-ref":"pkg:npm/jest-get-type@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-matcher-utils","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-matcher-utils@29.7.0","type":"library","bom-ref":"pkg:npm/jest-matcher-utils@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-diff","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-diff@29.7.0","type":"library","bom-ref":"pkg:npm/jest-diff@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"diff-sequences","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/diff-sequences@29.6.3","type":"library","bom-ref":"pkg:npm/diff-sequences@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-snapshot","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-snapshot@29.7.0","type":"library","bom-ref":"pkg:npm/jest-snapshot@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-jsx","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-jsx@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-jsx@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@babel","name":"plugin-syntax-typescript","version":"7.29.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40babel/plugin-syntax-typescript@7.29.7","type":"library","bom-ref":"pkg:npm/@babel/plugin-syntax-typescript@7.29.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"natural-compare","version":"1.4.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/natural-compare@1.4.0","type":"library","bom-ref":"pkg:npm/natural-compare@1.4.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"co","version":"4.6.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/co@4.6.0","type":"library","bom-ref":"pkg:npm/co@4.6.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"dedent","version":"1.7.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/dedent@1.7.2","type":"library","bom-ref":"pkg:npm/dedent@1.7.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-generator-fn","version":"2.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-generator-fn@2.1.0","type":"library","bom-ref":"pkg:npm/is-generator-fn@2.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-each","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-each@29.7.0","type":"library","bom-ref":"pkg:npm/jest-each@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-runtime","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-runtime@29.7.0","type":"library","bom-ref":"pkg:npm/jest-runtime@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"globals","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/globals@29.7.0","type":"library","bom-ref":"pkg:npm/@jest/globals@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@jest","name":"source-map","version":"29.6.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40jest/source-map@29.6.3","type":"library","bom-ref":"pkg:npm/@jest/source-map@29.6.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"callsites","version":"3.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/callsites@3.1.0","type":"library","bom-ref":"pkg:npm/callsites@3.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cjs-module-lexer","version":"1.4.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/cjs-module-lexer@1.4.3","type":"library","bom-ref":"pkg:npm/cjs-module-lexer@1.4.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-resolve","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-resolve@29.7.0","type":"library","bom-ref":"pkg:npm/jest-resolve@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-pnp-resolver","version":"1.2.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-pnp-resolver@1.2.3","type":"library","bom-ref":"pkg:npm/jest-pnp-resolver@1.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-validate","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-validate@29.7.0","type":"library","bom-ref":"pkg:npm/jest-validate@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"camelcase","version":"6.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/camelcase@6.3.0","type":"library","bom-ref":"pkg:npm/camelcase@6.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"leven","version":"3.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/leven@3.1.0","type":"library","bom-ref":"pkg:npm/leven@3.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"resolve","version":"1.22.12","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/resolve@1.22.12","type":"library","bom-ref":"pkg:npm/resolve@1.22.12","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-core-module","version":"2.16.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-core-module@2.16.2","type":"library","bom-ref":"pkg:npm/is-core-module@2.16.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"path-parse","version":"1.0.7","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/path-parse@1.0.7","type":"library","bom-ref":"pkg:npm/path-parse@1.0.7","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"supports-preserve-symlinks-flag","version":"1.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/supports-preserve-symlinks-flag@1.0.0","type":"library","bom-ref":"pkg:npm/supports-preserve-symlinks-flag@1.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"resolve.exports","version":"2.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/resolve.exports@2.0.3","type":"library","bom-ref":"pkg:npm/resolve.exports@2.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"strip-bom","version":"4.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/strip-bom@4.0.0","type":"library","bom-ref":"pkg:npm/strip-bom@4.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"pure-rand","version":"6.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/pure-rand@6.1.0","type":"library","bom-ref":"pkg:npm/pure-rand@6.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-environment-node","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-environment-node@29.7.0","type":"library","bom-ref":"pkg:npm/jest-environment-node@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-runner","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-runner@29.7.0","type":"library","bom-ref":"pkg:npm/jest-runner@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"emittery","version":"0.13.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/emittery@0.13.1","type":"library","bom-ref":"pkg:npm/emittery@0.13.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-docblock","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-docblock@29.7.0","type":"library","bom-ref":"pkg:npm/jest-docblock@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"detect-newline","version":"3.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/detect-newline@3.1.0","type":"library","bom-ref":"pkg:npm/detect-newline@3.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-leak-detector","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-leak-detector@29.7.0","type":"library","bom-ref":"pkg:npm/jest-leak-detector@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-watcher","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-watcher@29.7.0","type":"library","bom-ref":"pkg:npm/jest-watcher@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"source-map-support","version":"0.5.13","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/source-map-support@0.5.13","type":"library","bom-ref":"pkg:npm/source-map-support@0.5.13","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"buffer-from","version":"1.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/buffer-from@1.1.2","type":"library","bom-ref":"pkg:npm/buffer-from@1.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"parse-json","version":"5.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/parse-json@5.2.0","type":"library","bom-ref":"pkg:npm/parse-json@5.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"error-ex","version":"1.3.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/error-ex@1.3.4","type":"library","bom-ref":"pkg:npm/error-ex@1.3.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-arrayish","version":"0.2.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-arrayish@0.2.1","type":"library","bom-ref":"pkg:npm/is-arrayish@0.2.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"json-parse-even-better-errors","version":"2.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/json-parse-even-better-errors@2.3.1","type":"library","bom-ref":"pkg:npm/json-parse-even-better-errors@2.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"lines-and-columns","version":"1.2.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/lines-and-columns@1.2.4","type":"library","bom-ref":"pkg:npm/lines-and-columns@1.2.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"strip-json-comments","version":"3.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/strip-json-comments@3.1.1","type":"library","bom-ref":"pkg:npm/strip-json-comments@3.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-resolve-dependencies","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-resolve-dependencies@29.7.0","type":"library","bom-ref":"pkg:npm/jest-resolve-dependencies@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"import-local","version":"3.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/import-local@3.2.0","type":"library","bom-ref":"pkg:npm/import-local@3.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"pkg-dir","version":"4.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/pkg-dir@4.2.0","type":"library","bom-ref":"pkg:npm/pkg-dir@4.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"resolve-cwd","version":"3.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/resolve-cwd@3.0.0","type":"library","bom-ref":"pkg:npm/resolve-cwd@3.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"jest-cli","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/jest-cli@29.7.0","type":"library","bom-ref":"pkg:npm/jest-cli@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"create-jest","version":"29.7.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/create-jest@29.7.0","type":"library","bom-ref":"pkg:npm/create-jest@29.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"prompts","version":"2.4.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/prompts@2.4.2","type":"library","bom-ref":"pkg:npm/prompts@2.4.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"kleur","version":"3.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/kleur@3.0.3","type":"library","bom-ref":"pkg:npm/kleur@3.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"sisteransi","version":"1.0.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/sisteransi@1.0.5","type":"library","bom-ref":"pkg:npm/sisteransi@1.0.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"yargs","version":"17.7.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/yargs@17.7.3","type":"library","bom-ref":"pkg:npm/yargs@17.7.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cliui","version":"8.0.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/cliui@8.0.1","type":"library","bom-ref":"pkg:npm/cliui@8.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"string-width","version":"4.2.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/string-width@4.2.3","type":"library","bom-ref":"pkg:npm/string-width@4.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"emoji-regex","version":"8.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/emoji-regex@8.0.0","type":"library","bom-ref":"pkg:npm/emoji-regex@8.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-fullwidth-code-point","version":"3.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-fullwidth-code-point@3.0.0","type":"library","bom-ref":"pkg:npm/is-fullwidth-code-point@3.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"wrap-ansi","version":"7.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/wrap-ansi@7.0.0","type":"library","bom-ref":"pkg:npm/wrap-ansi@7.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"get-caller-file","version":"2.0.5","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/get-caller-file@2.0.5","type":"library","bom-ref":"pkg:npm/get-caller-file@2.0.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"require-directory","version":"2.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/require-directory@2.1.1","type":"library","bom-ref":"pkg:npm/require-directory@2.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"y18n","version":"5.0.8","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/y18n@5.0.8","type":"library","bom-ref":"pkg:npm/y18n@5.0.8","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"yargs-parser","version":"21.1.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/yargs-parser@21.1.1","type":"library","bom-ref":"pkg:npm/yargs-parser@21.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"supertest","version":"6.3.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/supertest@6.3.4","type":"library","bom-ref":"pkg:npm/supertest@6.3.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"superagent","version":"8.1.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/superagent@8.1.2","type":"library","bom-ref":"pkg:npm/superagent@8.1.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"component-emitter","version":"1.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/component-emitter@1.3.1","type":"library","bom-ref":"pkg:npm/component-emitter@1.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cookiejar","version":"2.1.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/cookiejar@2.1.4","type":"library","bom-ref":"pkg:npm/cookiejar@2.1.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fast-safe-stringify","version":"2.1.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fast-safe-stringify@2.1.1","type":"library","bom-ref":"pkg:npm/fast-safe-stringify@2.1.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"formidable","version":"2.1.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/formidable@2.1.5","type":"library","bom-ref":"pkg:npm/formidable@2.1.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@paralleldrive","name":"cuid2","version":"2.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40paralleldrive/cuid2@2.3.1","type":"library","bom-ref":"pkg:npm/@paralleldrive/cuid2@2.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@noble","name":"hashes","version":"1.8.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40noble/hashes@1.8.0","type":"library","bom-ref":"pkg:npm/@noble/hashes@1.8.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"dezalgo","version":"1.0.4","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/dezalgo@1.0.4","type":"library","bom-ref":"pkg:npm/dezalgo@1.0.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"asap","version":"2.0.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/asap@2.0.6","type":"library","bom-ref":"pkg:npm/asap@2.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"mime","version":"2.6.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/mime@2.6.0","type":"library","bom-ref":"pkg:npm/mime@2.6.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"eslint","version":"8.57.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/eslint@8.57.1","type":"library","bom-ref":"pkg:npm/eslint@8.57.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@eslint-community","name":"eslint-utils","version":"4.9.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40eslint-community/eslint-utils@4.9.1","type":"library","bom-ref":"pkg:npm/@eslint-community/eslint-utils@4.9.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"eslint-visitor-keys","version":"3.4.3","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/eslint-visitor-keys@3.4.3","type":"library","bom-ref":"pkg:npm/eslint-visitor-keys@3.4.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@eslint-community","name":"regexpp","version":"4.12.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40eslint-community/regexpp@4.12.2","type":"library","bom-ref":"pkg:npm/@eslint-community/regexpp@4.12.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@eslint","name":"eslintrc","version":"2.1.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40eslint/eslintrc@2.1.4","type":"library","bom-ref":"pkg:npm/@eslint/eslintrc@2.1.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ajv","version":"6.15.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ajv@6.15.0","type":"library","bom-ref":"pkg:npm/ajv@6.15.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fast-deep-equal","version":"3.1.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fast-deep-equal@3.1.3","type":"library","bom-ref":"pkg:npm/fast-deep-equal@3.1.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"json-schema-traverse","version":"0.4.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/json-schema-traverse@0.4.1","type":"library","bom-ref":"pkg:npm/json-schema-traverse@0.4.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"uri-js","version":"4.4.1","scope":"optional","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/uri-js@4.4.1","type":"library","bom-ref":"pkg:npm/uri-js@4.4.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"punycode","version":"2.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/punycode@2.3.1","type":"library","bom-ref":"pkg:npm/punycode@2.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"espree","version":"9.6.1","scope":"optional","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/espree@9.6.1","type":"library","bom-ref":"pkg:npm/espree@9.6.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"acorn","version":"8.17.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/acorn@8.17.0","type":"library","bom-ref":"pkg:npm/acorn@8.17.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"acorn-jsx","version":"5.3.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/acorn-jsx@5.3.2","type":"library","bom-ref":"pkg:npm/acorn-jsx@5.3.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"globals","version":"13.24.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/globals@13.24.0","type":"library","bom-ref":"pkg:npm/globals@13.24.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"type-fest","version":"0.20.2","scope":"optional","licenses":[{"expression":"(MIT OR CC0-1.0)"}],"purl":"pkg:npm/type-fest@0.20.2","type":"library","bom-ref":"pkg:npm/type-fest@0.20.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"ignore","version":"5.3.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/ignore@5.3.2","type":"library","bom-ref":"pkg:npm/ignore@5.3.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"import-fresh","version":"3.3.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/import-fresh@3.3.1","type":"library","bom-ref":"pkg:npm/import-fresh@3.3.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"parent-module","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/parent-module@1.0.1","type":"library","bom-ref":"pkg:npm/parent-module@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"resolve-from","version":"4.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/resolve-from@4.0.0","type":"library","bom-ref":"pkg:npm/resolve-from@4.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"js-yaml","version":"4.3.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/js-yaml@4.3.0","type":"library","bom-ref":"pkg:npm/js-yaml@4.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"argparse","version":"2.0.1","scope":"optional","licenses":[{"license":{"id":"Python-2.0","url":"https://opensource.org/licenses/Python-2.0"}}],"purl":"pkg:npm/argparse@2.0.1","type":"library","bom-ref":"pkg:npm/argparse@2.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@eslint","name":"js","version":"8.57.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40eslint/js@8.57.1","type":"library","bom-ref":"pkg:npm/@eslint/js@8.57.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@humanwhocodes","name":"config-array","version":"0.13.0","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/%40humanwhocodes/config-array@0.13.0","type":"library","bom-ref":"pkg:npm/@humanwhocodes/config-array@0.13.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@humanwhocodes","name":"object-schema","version":"2.0.3","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/%40humanwhocodes/object-schema@2.0.3","type":"library","bom-ref":"pkg:npm/@humanwhocodes/object-schema@2.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@humanwhocodes","name":"module-importer","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/%40humanwhocodes/module-importer@1.0.1","type":"library","bom-ref":"pkg:npm/@humanwhocodes/module-importer@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@nodelib","name":"fs.walk","version":"1.2.8","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40nodelib/fs.walk@1.2.8","type":"library","bom-ref":"pkg:npm/@nodelib/fs.walk@1.2.8","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@nodelib","name":"fs.scandir","version":"2.1.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40nodelib/fs.scandir@2.1.5","type":"library","bom-ref":"pkg:npm/@nodelib/fs.scandir@2.1.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@nodelib","name":"fs.stat","version":"2.0.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/%40nodelib/fs.stat@2.0.5","type":"library","bom-ref":"pkg:npm/@nodelib/fs.stat@2.0.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"run-parallel","version":"1.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/run-parallel@1.2.0","type":"library","bom-ref":"pkg:npm/run-parallel@1.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"queue-microtask","version":"1.2.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/queue-microtask@1.2.3","type":"library","bom-ref":"pkg:npm/queue-microtask@1.2.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fastq","version":"1.20.1","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/fastq@1.20.1","type":"library","bom-ref":"pkg:npm/fastq@1.20.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"reusify","version":"1.1.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/reusify@1.1.0","type":"library","bom-ref":"pkg:npm/reusify@1.1.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"@ungap","name":"structured-clone","version":"1.3.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/%40ungap/structured-clone@1.3.2","type":"library","bom-ref":"pkg:npm/@ungap/structured-clone@1.3.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"doctrine","version":"3.0.0","scope":"optional","licenses":[{"license":{"id":"Apache-2.0","url":"https://opensource.org/licenses/Apache-2.0"}}],"purl":"pkg:npm/doctrine@3.0.0","type":"library","bom-ref":"pkg:npm/doctrine@3.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"esutils","version":"2.0.3","scope":"optional","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/esutils@2.0.3","type":"library","bom-ref":"pkg:npm/esutils@2.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"escape-string-regexp","version":"4.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/escape-string-regexp@4.0.0","type":"library","bom-ref":"pkg:npm/escape-string-regexp@4.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"eslint-scope","version":"7.2.2","scope":"optional","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/eslint-scope@7.2.2","type":"library","bom-ref":"pkg:npm/eslint-scope@7.2.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"esrecurse","version":"4.3.0","scope":"optional","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/esrecurse@4.3.0","type":"library","bom-ref":"pkg:npm/esrecurse@4.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"estraverse","version":"5.3.0","scope":"optional","licenses":[{"license":{"id":"BSD-2-Clause","url":"https://opensource.org/licenses/BSD-2-Clause"}}],"purl":"pkg:npm/estraverse@5.3.0","type":"library","bom-ref":"pkg:npm/estraverse@5.3.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"esquery","version":"1.7.0","scope":"optional","licenses":[{"license":{"id":"BSD-3-Clause","url":"https://opensource.org/licenses/BSD-3-Clause"}}],"purl":"pkg:npm/esquery@1.7.0","type":"library","bom-ref":"pkg:npm/esquery@1.7.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"file-entry-cache","version":"6.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/file-entry-cache@6.0.1","type":"library","bom-ref":"pkg:npm/file-entry-cache@6.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"flat-cache","version":"3.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/flat-cache@3.2.0","type":"library","bom-ref":"pkg:npm/flat-cache@3.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"flatted","version":"3.4.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/flatted@3.4.2","type":"library","bom-ref":"pkg:npm/flatted@3.4.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"keyv","version":"4.5.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/keyv@4.5.4","type":"library","bom-ref":"pkg:npm/keyv@4.5.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"json-buffer","version":"3.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/json-buffer@3.0.1","type":"library","bom-ref":"pkg:npm/json-buffer@3.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"rimraf","version":"3.0.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/rimraf@3.0.2","type":"library","bom-ref":"pkg:npm/rimraf@3.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"find-up","version":"5.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/find-up@5.0.0","type":"library","bom-ref":"pkg:npm/find-up@5.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"locate-path","version":"6.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/locate-path@6.0.0","type":"library","bom-ref":"pkg:npm/locate-path@6.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"p-locate","version":"5.0.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/p-locate@5.0.0","type":"library","bom-ref":"pkg:npm/p-locate@5.0.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"glob-parent","version":"6.0.2","scope":"optional","licenses":[{"license":{"id":"ISC","url":"https://opensource.org/licenses/ISC"}}],"purl":"pkg:npm/glob-parent@6.0.2","type":"library","bom-ref":"pkg:npm/glob-parent@6.0.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"graphemer","version":"1.4.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/graphemer@1.4.0","type":"library","bom-ref":"pkg:npm/graphemer@1.4.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"is-path-inside","version":"3.0.3","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/is-path-inside@3.0.3","type":"library","bom-ref":"pkg:npm/is-path-inside@3.0.3","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"json-stable-stringify-without-jsonify","version":"1.0.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/json-stable-stringify-without-jsonify@1.0.1","type":"library","bom-ref":"pkg:npm/json-stable-stringify-without-jsonify@1.0.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"levn","version":"0.4.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/levn@0.4.1","type":"library","bom-ref":"pkg:npm/levn@0.4.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"prelude-ls","version":"1.2.1","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/prelude-ls@1.2.1","type":"library","bom-ref":"pkg:npm/prelude-ls@1.2.1","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"type-check","version":"0.4.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/type-check@0.4.0","type":"library","bom-ref":"pkg:npm/type-check@0.4.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"lodash.merge","version":"4.6.2","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/lodash.merge@4.6.2","type":"library","bom-ref":"pkg:npm/lodash.merge@4.6.2","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"optionator","version":"0.9.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/optionator@0.9.4","type":"library","bom-ref":"pkg:npm/optionator@0.9.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"deep-is","version":"0.1.4","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/deep-is@0.1.4","type":"library","bom-ref":"pkg:npm/deep-is@0.1.4","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"fast-levenshtein","version":"2.0.6","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/fast-levenshtein@2.0.6","type":"library","bom-ref":"pkg:npm/fast-levenshtein@2.0.6","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"word-wrap","version":"1.2.5","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/word-wrap@1.2.5","type":"library","bom-ref":"pkg:npm/word-wrap@1.2.5","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"text-table","version":"0.2.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/text-table@0.2.0","type":"library","bom-ref":"pkg:npm/text-table@0.2.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"prettier","version":"3.9.0","scope":"optional","licenses":[{"license":{"id":"MIT","url":"https://opensource.org/licenses/MIT"}}],"purl":"pkg:npm/prettier@3.9.0","type":"library","bom-ref":"pkg:npm/prettier@3.9.0","properties":[{"name":"SrcFile","value":"package-lock.json"},{"name":"cdx:npm:package:development","value":"true"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/nodejs/package-lock.json"}]}}},{"group":"","name":"cfhdojbkjhnklbpkdaibdccddilifddb","version":"4.41.2","description":"__MSG_name_releasebuild__","purl":"pkg:chrome-extension/cfhdojbkjhnklbpkdaibdccddilifddb@4.41.2","type":"application","bom-ref":"pkg:chrome-extension/cfhdojbkjhnklbpkdaibdccddilifddb@4.41.2","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"alarms, contextMenus, declarativeNetRequestWithHostAccess, notifications, scripting, storage, tabs, unlimitedStorage, webNavigation, webRequest"},{"name":"cdx:chrome-extension:optionalPermissions","value":"contentSettings, management"},{"name":"cdx:chrome-extension:hostPermissions","value":", http://*/*, https://*/*, *://*.youtube.com/*, https://accounts.adblockplus.org/*, https://*.myaccount.adblockplus.org/*, https://adblockplus.org/*, https://new.adblockplus.org/*, https://welcome.adblockplus.org/*, https://getadblock.com/*, https://vpn.getadblock.com/*, https://*.myaccount.getadblock.com/*, https://btloader.com/trustedIframe.html, *://*.facebook.com/*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start, document_idle"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"managed-storage-schema.json"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"124.0"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/cfhdojbkjhnklbpkdaibdccddilifddb/4.41.2_0/manifest.json"}]},{"group":"","name":"egaejpfbkjamgheoingidhokbfnidlpi","version":"1.6","description":"Symantec Extension","purl":"pkg:chrome-extension/egaejpfbkjamgheoingidhokbfnidlpi@1.6","type":"application","bom-ref":"pkg:chrome-extension/egaejpfbkjamgheoingidhokbfnidlpi@1.6","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"nativeMessaging, tabs, scripting"},{"name":"cdx:chrome-extension:hostPermissions","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/egaejpfbkjamgheoingidhokbfnidlpi/1.6_0/manifest.json"}]},{"group":"","name":"emnphkkblegpebimobpbekeedfgemhof","version":"1.6.10","description":"Native HLS Playback","purl":"pkg:chrome-extension/emnphkkblegpebimobpbekeedfgemhof@1.6.10","type":"application","bom-ref":"pkg:chrome-extension/emnphkkblegpebimobpbekeedfgemhof@1.6.10","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"tabs, storage, scripting, activeTab, webNavigation"},{"name":"cdx:chrome-extension:hostPermissions","value":""},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'; connect-src *\"}"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/emnphkkblegpebimobpbekeedfgemhof/1.6.10_0/manifest.json"}]},{"group":"","name":"fdpohaocaechififmbbbbbknoalclacl","version":"8.6","description":"GoFullPage - Full Page Screen Capture","purl":"pkg:chrome-extension/fdpohaocaechififmbbbbbknoalclacl@8.6","type":"application","bom-ref":"pkg:chrome-extension/fdpohaocaechififmbbbbbknoalclacl@8.6","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"activeTab, scripting, storage, unlimitedStorage"},{"name":"cdx:chrome-extension:optionalPermissions","value":"downloads, webNavigation"},{"name":"cdx:chrome-extension:optionalHostPermissions","value":", file://*/*"},{"name":"cdx:chrome-extension:commands","value":"_execute_action"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"102"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":"*://*.gofullpage.com/*, https://localhost:1234/*, https://dev.d32cgdvim65k7p.amplifyapp.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"fileAccess, network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:fileAccess","value":"true"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/fdpohaocaechififmbbbbbknoalclacl/8.6_0/manifest.json"}]},{"group":"","name":"ghbmnnjooekpmoecnnnilnnbdlolhkhi","version":"1.107.1","description":"__MSG_extName__","purl":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.107.1","type":"application","bom-ref":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.107.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"alarms, storage, unlimitedStorage, offscreen"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://docs.google.com/*, https://drive.google.com/*"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'\"}"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"dasherSettingSchema.json"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"88"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:externallyConnectableMatches","value":"https://docs.google.com/*, https://drive.google.com/*, https://drive-autopush.corp.google.com/*, https://drive-daily-0.corp.google.com/*, https://drive-daily-1.corp.google.com/*, https://drive-daily-2.corp.google.com/*, https://drive-daily-3.corp.google.com/*, https://drive-daily-4.corp.google.com/*, https://drive-daily-5.corp.google.com/*, https://drive-daily-6.corp.google.com/*, https://drive-preprod.corp.google.com/*, https://drive-staging.corp.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.107.1_0/manifest.json"}]},{"group":"","name":"hahkjjkedonglpienpfiganogikkkoii","version":"1.7","description":"Play HLS","purl":"pkg:chrome-extension/hahkjjkedonglpienpfiganogikkkoii@1.7","type":"application","bom-ref":"pkg:chrome-extension/hahkjjkedonglpienpfiganogikkkoii@1.7","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"*://*/*m3u8*, *://*/*.ts*, webRequest, webRequestBlocking"},{"name":"cdx:chrome-extension:hostPermissions","value":"*://*/*.m3u8*, *://*/*.ts*"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hahkjjkedonglpienpfiganogikkkoii/1.7_0/manifest.json"}]},{"group":"","name":"hmjkmjkepdijhoojdojkdfohbdgmmhki","version":"4.26091.600.1","description":"__MSG_appName__","purl":"pkg:chrome-extension/hmjkmjkepdijhoojdojkdfohbdgmmhki@4.26091.600.1","type":"application","bom-ref":"pkg:chrome-extension/hmjkmjkepdijhoojdojkdfohbdgmmhki@4.26091.600.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"fileSystem, identity, identity.email, unlimitedStorage, https://*.googleapis.com/, https://keep.google.com/media/, https://*.googleusercontent.com/, https://*.client-channel.google.com/client-channel, https://clients4.google.com/client-channel/client, https://www.google-analytics.com/, https://www.google.com/, https://play.google.com/log, geolocation, management, notifications"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"57"},{"name":"cdx:chrome-extension:offlineEnabled","value":"true"},{"name":"cdx:chrome-extension:capabilities","value":"fileAccess, network, fingerprinting"},{"name":"cdx:chrome-extension:capability:fileAccess","value":"true"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hmjkmjkepdijhoojdojkdfohbdgmmhki/4.26091.600.1_0/manifest.json"}]},{"group":"","name":"hoombieeljmmljlkjmnheibnpciblicm","version":"5.1.8","description":"Language Reactor","purl":"pkg:chrome-extension/hoombieeljmmljlkjmnheibnpciblicm@5.1.8","type":"application","bom-ref":"pkg:chrome-extension/hoombieeljmmljlkjmnheibnpciblicm@5.1.8","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"storage, contextMenus, activeTab, scripting"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://i.ytimg.com/*, https://*.netflix.com/*, *://*.youtube.com/*, *://*.amazon.com.br/*, *://*.amazon.ca/*, *://*.amazon.com.mx/*, *://*.amazon.com/*, *://*.amazon.cn/*, *://*.amazon.in/*, *://*.amazon.co.jp/*, *://*.amazon.sg/*, *://*.amazon.ae/*, *://*.amazon.sa/*, *://*.amazon.fr/*, *://*.amazon.de/*, *://*.amazon.it/*, *://*.amazon.nl/*, *://*.amazon.pl/*, *://*.amazon.es/*, *://*.amazon.se/*, *://*.amazon.com.tr/*, *://*.amazon.co.uk/*, *://*.amazon.com.au/*, https://*.languagereactor.com/*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'; img-src 'self' https://www.gstatic.com/ https://cdn.firebase.com;/ connect-src 'self' https://*.firebaseio.com/ https://api.dioco.io/ https://www.youtube.com/ https://www.googleapis.com/ https://securetoken.googleapis.com/ https://us-central1-nlle-b0128.cloudfunctions.net/ https://apis.google.com/ https://identitytoolkit.googleapis.com/ https://i.ytimg.com/\"}"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/hoombieeljmmljlkjmnheibnpciblicm/5.1.8_0/manifest.json"}]},{"group":"","name":"ijdefdijdmghafocfmmdojfghnpelnfn","version":"3.33.1","description":"__MSG_extName__","purl":"pkg:chrome-extension/ijdefdijdmghafocfmmdojfghnpelnfn@3.33.1","type":"application","bom-ref":"pkg:chrome-extension/ijdefdijdmghafocfmmdojfghnpelnfn@3.33.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"activeTab, storage, cookies, scripting, declarativeNetRequestWithHostAccess"},{"name":"cdx:chrome-extension:optionalPermissions","value":"tabs"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://notebooklm.google.com/*, https://clerk.notebooklm-web-importer.com/*"},{"name":"cdx:chrome-extension:optionalHostPermissions","value":"https://*/*, http://*/*"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ijdefdijdmghafocfmmdojfghnpelnfn/3.33.1_0/manifest.json"}]},{"group":"","name":"inomeogfingihgjfjlpeplalcfajhgai","version":"2.1","description":"Chrome Remote Desktop","purl":"pkg:chrome-extension/inomeogfingihgjfjlpeplalcfajhgai@2.1","type":"application","bom-ref":"pkg:chrome-extension/inomeogfingihgjfjlpeplalcfajhgai@2.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"nativeMessaging, downloads"},{"name":"cdx:chrome-extension:externallyConnectableMatches","value":"https://remotedesktop.corp.google.com/*, https://remotedesktop.google.com/*, https://remotedesktop-dev.corp.google.com/*, https://remotedesktop-autopush.corp.google.com/*, https://remotedesktop-daily-0.corp.google.com/*, https://remotedesktop-daily-1.corp.google.com/*, https://remotedesktop-daily-2.corp.google.com/*, https://remotedesktop-daily-3.corp.google.com/*, https://remotedesktop-daily-4.corp.google.com/*, https://remotedesktop-daily-5.corp.google.com/*, https://remotedesktop-daily-6.corp.google.com/*, https://remotesupport.corp.google.com/*, https://remotesupport-dev.corp.google.com/*, https://remotesupport-autopush.corp.google.com/*, https://remotesupport-daily-0.corp.google.com/*, https://remotesupport-daily-1.corp.google.com/*, https://remotesupport-daily-2.corp.google.com/*, https://remotesupport-daily-3.corp.google.com/*, https://remotesupport-daily-4.corp.google.com/*, https://remotesupport-daily-5.corp.google.com/*, https://remotesupport-daily-6.corp.google.com/*, https://remoting.sandbox.google.com/*, https://remoting-autopush.sandbox.google.com/*, https://remoting-daily-0.sandbox.google.com/*, https://remoting-daily-1.sandbox.google.com/*, https://remoting-daily-2.sandbox.google.com/*, https://remoting-daily-3.sandbox.google.com/*, https://remoting-daily-4.sandbox.google.com/*, https://remoting-daily-5.sandbox.google.com/*, https://remoting-daily-6.sandbox.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"fileAccess"},{"name":"cdx:chrome-extension:capability:fileAccess","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/inomeogfingihgjfjlpeplalcfajhgai/2.1_0/manifest.json"}]},{"group":"","name":"kobncfkmjelbefaoohoblamnbackjggk","version":"1.0.30","description":"__MSG_appName__","purl":"pkg:chrome-extension/kobncfkmjelbefaoohoblamnbackjggk@1.0.30","type":"application","bom-ref":"pkg:chrome-extension/kobncfkmjelbefaoohoblamnbackjggk@1.0.30","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"tabs, storage, contextMenus, scripting, unlimitedStorage"},{"name":"cdx:chrome-extension:hostPermissions","value":", *://*/*, https://*.youtube.com/*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/kobncfkmjelbefaoohoblamnbackjggk/1.0.30_0/manifest.json"}]},{"group":"","name":"lkcdojpmjehlniamnglpjlldkoonlomb","version":"1.0.0.5","description":"__MSG_ext_name__","purl":"pkg:chrome-extension/lkcdojpmjehlniamnglpjlldkoonlomb@1.0.0.5","type":"application","bom-ref":"pkg:chrome-extension/lkcdojpmjehlniamnglpjlldkoonlomb@1.0.0.5","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"http://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"notifications, unlimitedStorage"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/lkcdojpmjehlniamnglpjlldkoonlomb/1.0.0.5_0/manifest.json"}]},{"group":"","name":"ndnaehgpjlnokgebbaldlmgkapkpjkkb","version":"12.82.0","description":"__MSG_appName__","purl":"pkg:chrome-extension/ndnaehgpjlnokgebbaldlmgkapkpjkkb@12.82.0","type":"application","bom-ref":"pkg:chrome-extension/ndnaehgpjlnokgebbaldlmgkapkpjkkb@12.82.0","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"storage, declarativeNetRequestWithHostAccess, notifications"},{"name":"cdx:chrome-extension:hostPermissions","value":"*://*.mailtrack.io/*, *://mailtrack.io/*, *://*.mailsuite.com/*, *://mailsuite.com/*, https://mail.google.com/*, *://*.googleusercontent.com/*, https://api.rollbar.com/*, https://http-intake.logs.datadoghq.eu/*, https://mrt-attachments-prod.s3.eu-west-1.amazonaws.com/*, https://mt-file-tracking-temp.s3.eu-west-1.amazonaws.com/*, *://mailtrack.io/*/dashboard/welcome*, *://mailtrack.io/*/dashboard/reauthorized*, *://mailtrack.io/*/dashboard/install-success*, *://mailtrack.io/*/dashboard/payment/teams/success*, *://mailsuite.com/*/dashboard/welcome*, *://mailsuite.com/*/dashboard/reauthorized*, *://mailsuite.com/*/dashboard/install-success*, *://mailsuite.com/*/dashboard/payment/teams/success*"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start, document_end"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'\"}"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":"https://mail.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/ndnaehgpjlnokgebbaldlmgkapkpjkkb/12.82.0_0/manifest.json"}]},{"group":"","name":"nlbgclfdbneidpjhchcipedhbffffghm","version":"1.0.1.7","description":"__MSG_ext_name__","purl":"pkg:chrome-extension/nlbgclfdbneidpjhchcipedhbffffghm@1.0.1.7","type":"application","bom-ref":"pkg:chrome-extension/nlbgclfdbneidpjhchcipedhbffffghm@1.0.1.7","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"http://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"unlimitedStorage"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nlbgclfdbneidpjhchcipedhbffffghm/1.0.1.7_0/manifest.json"}]},{"group":"","name":"nmmhkkegccagdldgiimedpiccmgmieda","version":"1.0.0.6","description":"__MSG_APP_NAME__","purl":"pkg:chrome-extension/nmmhkkegccagdldgiimedpiccmgmieda@1.0.0.6","type":"application","bom-ref":"pkg:chrome-extension/nmmhkkegccagdldgiimedpiccmgmieda@1.0.0.6","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"2"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"identity, webview, https://www.google.com/, https://www.googleapis.com/*, https://payments.google.com/payments/v4/js/integrator.js, https://sandbox.google.com/payments/v4/js/integrator.js"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"29"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/nmmhkkegccagdldgiimedpiccmgmieda/1.0.0.6_0/manifest.json"}]},{"group":"","name":"oeopbcgkkoapgobdbedcemjljbihmemj","version":"36.2","description":"Checker Plus for Gmail™","purl":"pkg:chrome-extension/oeopbcgkkoapgobdbedcemjljbihmemj@36.2","type":"application","bom-ref":"pkg:chrome-extension/oeopbcgkkoapgobdbedcemjljbihmemj@36.2","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Default"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"gcm, alarms, activeTab, tts, idle, notifications, storage, contextMenus, identity, system.display, cookies, declarativeNetRequestWithHostAccess, offscreen, scripting, sidePanel"},{"name":"cdx:chrome-extension:optionalPermissions","value":"background, webRequest"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://mail.google.com/, https://mail.google.com/mail/mu/mp/*"},{"name":"cdx:chrome-extension:optionalHostPermissions","value":"https://*.googleusercontent.com/"},{"name":"cdx:chrome-extension:commands","value":"compose, dnd, markAllAsRead, markAsReadInNotificationWindow, openEmailDisplayedInNotificationWindow, quickComposeEmail, refresh"},{"name":"cdx:chrome-extension:contentScriptsRunAt","value":"document_start"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"schema.json"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":"https://jasonsavard.com/*, https://checkout.stripe.com/*, https://www.paypal.com/*, https://nowpayments.io/*, https://mail.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network, codeInjection, fingerprinting"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"cdx:chrome-extension:capability:codeInjection","value":"true"},{"name":"cdx:chrome-extension:capability:fingerprinting","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Default/Extensions/oeopbcgkkoapgobdbedcemjljbihmemj/36.2_0/manifest.json"}]},{"group":"","name":"ghbmnnjooekpmoecnnnilnnbdlolhkhi","version":"1.104.1","description":"__MSG_extName__","purl":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.104.1","type":"application","bom-ref":"pkg:chrome-extension/ghbmnnjooekpmoecnnnilnnbdlolhkhi@1.104.1","properties":[{"name":"cdx:chrome-extension:browser","value":"Google Chrome"},{"name":"cdx:chrome-extension:channel","value":"stable"},{"name":"cdx:chrome-extension:profile","value":"Profile 1"},{"name":"cdx:chrome-extension:profilePath","value":"/Users/1112821/Library/Application Support/Google/Chrome/Profile 1"},{"name":"cdx:chrome-extension:manifestVersion","value":"3"},{"name":"cdx:chrome-extension:updateUrl","value":"https://clients2.google.com/service/update2/crx"},{"name":"cdx:chrome-extension:permissions","value":"alarms, storage, unlimitedStorage, offscreen"},{"name":"cdx:chrome-extension:hostPermissions","value":"https://docs.google.com/*, https://drive.google.com/*"},{"name":"cdx:chrome-extension:contentSecurityPolicy","value":"{\"extension_pages\":\"script-src 'self'; object-src 'self'\"}"},{"name":"cdx:chrome-extension:storageManagedSchema","value":"dasherSettingSchema.json"},{"name":"cdx:chrome-extension:minimumChromeVersion","value":"88"},{"name":"cdx:chrome-extension:webAccessibleResourceMatches","value":""},{"name":"cdx:chrome-extension:externallyConnectableMatches","value":"https://docs.google.com/*, https://drive.google.com/*, https://drive-autopush.corp.google.com/*, https://drive-daily-0.corp.google.com/*, https://drive-daily-1.corp.google.com/*, https://drive-daily-2.corp.google.com/*, https://drive-daily-3.corp.google.com/*, https://drive-daily-4.corp.google.com/*, https://drive-daily-5.corp.google.com/*, https://drive-daily-6.corp.google.com/*, https://drive-preprod.corp.google.com/*, https://drive-staging.corp.google.com/*"},{"name":"cdx:chrome-extension:capabilities","value":"network"},{"name":"cdx:chrome-extension:capability:network","value":"true"},{"name":"SrcFile","value":"/Users/1112821/Library/Application Support/Google/Chrome/Profile 1/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.104.1_0/manifest.json"}]}],"services":[],"dependencies":[{"ref":"pkg:npm/mime-db@1.52.0","dependsOn":[]},{"ref":"pkg:npm/mime-types@2.1.35","dependsOn":["pkg:npm/mime-db@1.52.0"]},{"ref":"pkg:npm/negotiator@0.6.3","dependsOn":[]},{"ref":"pkg:npm/accepts@1.3.8","dependsOn":["pkg:npm/mime-types@2.1.35","pkg:npm/negotiator@0.6.3"]},{"ref":"pkg:npm/array-flatten@1.1.1","dependsOn":[]},{"ref":"pkg:npm/bytes@3.1.2","dependsOn":[]},{"ref":"pkg:npm/content-type@1.0.5","dependsOn":[]},{"ref":"pkg:npm/ms@2.0.0","dependsOn":[]},{"ref":"pkg:npm/debug@2.6.9","dependsOn":["pkg:npm/ms@2.0.0"]},{"ref":"pkg:npm/depd@2.0.0","dependsOn":[]},{"ref":"pkg:npm/destroy@1.2.0","dependsOn":[]},{"ref":"pkg:npm/inherits@2.0.4","dependsOn":[]},{"ref":"pkg:npm/setprototypeof@1.2.0","dependsOn":[]},{"ref":"pkg:npm/statuses@2.0.2","dependsOn":[]},{"ref":"pkg:npm/toidentifier@1.0.1","dependsOn":[]},{"ref":"pkg:npm/http-errors@2.0.1","dependsOn":["pkg:npm/depd@2.0.0","pkg:npm/inherits@2.0.4","pkg:npm/setprototypeof@1.2.0","pkg:npm/statuses@2.0.2","pkg:npm/toidentifier@1.0.1"]},{"ref":"pkg:npm/safer-buffer@2.1.2","dependsOn":[]},{"ref":"pkg:npm/iconv-lite@0.4.24","dependsOn":["pkg:npm/safer-buffer@2.1.2"]},{"ref":"pkg:npm/ee-first@1.1.1","dependsOn":[]},{"ref":"pkg:npm/on-finished@2.4.1","dependsOn":["pkg:npm/ee-first@1.1.1"]},{"ref":"pkg:npm/es-define-property@1.0.1","dependsOn":[]},{"ref":"pkg:npm/es-errors@1.3.0","dependsOn":[]},{"ref":"pkg:npm/object-inspect@1.13.4","dependsOn":[]},{"ref":"pkg:npm/side-channel-list@1.0.1","dependsOn":["pkg:npm/es-errors@1.3.0","pkg:npm/object-inspect@1.13.4"]},{"ref":"pkg:npm/function-bind@1.1.2","dependsOn":[]},{"ref":"pkg:npm/call-bind-apply-helpers@1.0.2","dependsOn":["pkg:npm/es-errors@1.3.0","pkg:npm/function-bind@1.1.2"]},{"ref":"pkg:npm/es-object-atoms@1.1.2","dependsOn":["pkg:npm/es-errors@1.3.0"]},{"ref":"pkg:npm/gopd@1.2.0","dependsOn":[]},{"ref":"pkg:npm/dunder-proto@1.0.1","dependsOn":["pkg:npm/call-bind-apply-helpers@1.0.2","pkg:npm/es-errors@1.3.0","pkg:npm/gopd@1.2.0"]},{"ref":"pkg:npm/get-proto@1.0.1","dependsOn":["pkg:npm/dunder-proto@1.0.1","pkg:npm/es-object-atoms@1.1.2"]},{"ref":"pkg:npm/has-symbols@1.1.0","dependsOn":[]},{"ref":"pkg:npm/hasown@2.0.4","dependsOn":["pkg:npm/function-bind@1.1.2"]},{"ref":"pkg:npm/math-intrinsics@1.1.0","dependsOn":[]},{"ref":"pkg:npm/get-intrinsic@1.3.0","dependsOn":["pkg:npm/call-bind-apply-helpers@1.0.2","pkg:npm/es-define-property@1.0.1","pkg:npm/es-errors@1.3.0","pkg:npm/es-object-atoms@1.1.2","pkg:npm/function-bind@1.1.2","pkg:npm/get-proto@1.0.1","pkg:npm/gopd@1.2.0","pkg:npm/has-symbols@1.1.0","pkg:npm/hasown@2.0.4","pkg:npm/math-intrinsics@1.1.0"]},{"ref":"pkg:npm/call-bound@1.0.4","dependsOn":["pkg:npm/call-bind-apply-helpers@1.0.2","pkg:npm/get-intrinsic@1.3.0"]},{"ref":"pkg:npm/side-channel-map@1.0.1","dependsOn":["pkg:npm/call-bound@1.0.4","pkg:npm/es-errors@1.3.0","pkg:npm/get-intrinsic@1.3.0","pkg:npm/object-inspect@1.13.4"]},{"ref":"pkg:npm/side-channel-weakmap@1.0.2","dependsOn":["pkg:npm/call-bound@1.0.4","pkg:npm/es-errors@1.3.0","pkg:npm/get-intrinsic@1.3.0","pkg:npm/object-inspect@1.13.4","pkg:npm/side-channel-map@1.0.1"]},{"ref":"pkg:npm/side-channel@1.1.1","dependsOn":["pkg:npm/es-errors@1.3.0","pkg:npm/object-inspect@1.13.4","pkg:npm/side-channel-list@1.0.1","pkg:npm/side-channel-map@1.0.1","pkg:npm/side-channel-weakmap@1.0.2"]},{"ref":"pkg:npm/qs@6.15.3","dependsOn":["pkg:npm/es-define-property@1.0.1","pkg:npm/side-channel@1.1.1"]},{"ref":"pkg:npm/unpipe@1.0.0","dependsOn":[]},{"ref":"pkg:npm/raw-body@2.5.3","dependsOn":["pkg:npm/bytes@3.1.2","pkg:npm/http-errors@2.0.1","pkg:npm/iconv-lite@0.4.24","pkg:npm/unpipe@1.0.0"]},{"ref":"pkg:npm/media-typer@0.3.0","dependsOn":[]},{"ref":"pkg:npm/type-is@1.6.18","dependsOn":["pkg:npm/media-typer@0.3.0","pkg:npm/mime-types@2.1.35"]},{"ref":"pkg:npm/body-parser@1.20.5","dependsOn":["pkg:npm/bytes@3.1.2","pkg:npm/content-type@1.0.5","pkg:npm/debug@2.6.9","pkg:npm/depd@2.0.0","pkg:npm/destroy@1.2.0","pkg:npm/http-errors@2.0.1","pkg:npm/iconv-lite@0.4.24","pkg:npm/on-finished@2.4.1","pkg:npm/qs@6.15.3","pkg:npm/raw-body@2.5.3","pkg:npm/type-is@1.6.18","pkg:npm/unpipe@1.0.0"]},{"ref":"pkg:npm/safe-buffer@5.2.1","dependsOn":[]},{"ref":"pkg:npm/content-disposition@0.5.4","dependsOn":["pkg:npm/safe-buffer@5.2.1"]},{"ref":"pkg:npm/cookie@0.7.2","dependsOn":[]},{"ref":"pkg:npm/cookie-signature@1.0.7","dependsOn":[]},{"ref":"pkg:npm/encodeurl@2.0.0","dependsOn":[]},{"ref":"pkg:npm/escape-html@1.0.3","dependsOn":[]},{"ref":"pkg:npm/etag@1.8.1","dependsOn":[]},{"ref":"pkg:npm/parseurl@1.3.3","dependsOn":[]},{"ref":"pkg:npm/finalhandler@1.3.2","dependsOn":["pkg:npm/debug@2.6.9","pkg:npm/encodeurl@2.0.0","pkg:npm/escape-html@1.0.3","pkg:npm/on-finished@2.4.1","pkg:npm/parseurl@1.3.3","pkg:npm/statuses@2.0.2","pkg:npm/unpipe@1.0.0"]},{"ref":"pkg:npm/fresh@0.5.2","dependsOn":[]},{"ref":"pkg:npm/merge-descriptors@1.0.3","dependsOn":[]},{"ref":"pkg:npm/methods@1.1.2","dependsOn":[]},{"ref":"pkg:npm/path-to-regexp@0.1.13","dependsOn":[]},{"ref":"pkg:npm/forwarded@0.2.0","dependsOn":[]},{"ref":"pkg:npm/ipaddr.js@1.9.1","dependsOn":[]},{"ref":"pkg:npm/proxy-addr@2.0.7","dependsOn":["pkg:npm/forwarded@0.2.0","pkg:npm/ipaddr.js@1.9.1"]},{"ref":"pkg:npm/range-parser@1.2.1","dependsOn":[]},{"ref":"pkg:npm/mime@1.6.0","dependsOn":[]},{"ref":"pkg:npm/ms@2.1.3","dependsOn":[]},{"ref":"pkg:npm/send@0.19.2","dependsOn":["pkg:npm/debug@2.6.9","pkg:npm/depd@2.0.0","pkg:npm/destroy@1.2.0","pkg:npm/encodeurl@2.0.0","pkg:npm/escape-html@1.0.3","pkg:npm/etag@1.8.1","pkg:npm/fresh@0.5.2","pkg:npm/http-errors@2.0.1","pkg:npm/mime@1.6.0","pkg:npm/ms@2.1.3","pkg:npm/on-finished@2.4.1","pkg:npm/range-parser@1.2.1","pkg:npm/statuses@2.0.2"]},{"ref":"pkg:npm/serve-static@1.16.3","dependsOn":["pkg:npm/encodeurl@2.0.0","pkg:npm/escape-html@1.0.3","pkg:npm/parseurl@1.3.3","pkg:npm/send@0.19.2"]},{"ref":"pkg:npm/utils-merge@1.0.1","dependsOn":[]},{"ref":"pkg:npm/vary@1.1.2","dependsOn":[]},{"ref":"pkg:npm/express@4.22.2","dependsOn":["pkg:npm/accepts@1.3.8","pkg:npm/array-flatten@1.1.1","pkg:npm/body-parser@1.20.5","pkg:npm/content-disposition@0.5.4","pkg:npm/content-type@1.0.5","pkg:npm/cookie-signature@1.0.7","pkg:npm/cookie@0.7.2","pkg:npm/debug@2.6.9","pkg:npm/depd@2.0.0","pkg:npm/encodeurl@2.0.0","pkg:npm/escape-html@1.0.3","pkg:npm/etag@1.8.1","pkg:npm/finalhandler@1.3.2","pkg:npm/fresh@0.5.2","pkg:npm/http-errors@2.0.1","pkg:npm/merge-descriptors@1.0.3","pkg:npm/methods@1.1.2","pkg:npm/on-finished@2.4.1","pkg:npm/parseurl@1.3.3","pkg:npm/path-to-regexp@0.1.13","pkg:npm/proxy-addr@2.0.7","pkg:npm/qs@6.15.3","pkg:npm/range-parser@1.2.1","pkg:npm/safe-buffer@5.2.1","pkg:npm/send@0.19.2","pkg:npm/serve-static@1.16.3","pkg:npm/setprototypeof@1.2.0","pkg:npm/statuses@2.0.2","pkg:npm/type-is@1.6.18","pkg:npm/utils-merge@1.0.1","pkg:npm/vary@1.1.2"]},{"ref":"pkg:npm/object-assign@4.1.1","dependsOn":[]},{"ref":"pkg:npm/cors@2.8.6","dependsOn":["pkg:npm/object-assign@4.1.1","pkg:npm/vary@1.1.2"]},{"ref":"pkg:npm/helmet@7.2.0","dependsOn":[]},{"ref":"pkg:npm/safe-buffer@5.1.2","dependsOn":[]},{"ref":"pkg:npm/basic-auth@2.0.1","dependsOn":["pkg:npm/safe-buffer@5.1.2"]},{"ref":"pkg:npm/on-headers@1.1.0","dependsOn":[]},{"ref":"pkg:npm/morgan@1.11.0","dependsOn":["pkg:npm/basic-auth@2.0.1","pkg:npm/debug@2.6.9","pkg:npm/depd@2.0.0","pkg:npm/on-finished@2.4.1","pkg:npm/on-headers@1.1.0"]},{"ref":"pkg:npm/dotenv@16.6.1","dependsOn":[]},{"ref":"pkg:npm/follow-redirects@1.16.0","dependsOn":[]},{"ref":"pkg:npm/asynckit@0.4.0","dependsOn":[]},{"ref":"pkg:npm/delayed-stream@1.0.0","dependsOn":[]},{"ref":"pkg:npm/combined-stream@1.0.8","dependsOn":["pkg:npm/delayed-stream@1.0.0"]},{"ref":"pkg:npm/has-tostringtag@1.0.2","dependsOn":["pkg:npm/has-symbols@1.1.0"]},{"ref":"pkg:npm/es-set-tostringtag@2.1.0","dependsOn":["pkg:npm/es-errors@1.3.0","pkg:npm/get-intrinsic@1.3.0","pkg:npm/has-tostringtag@1.0.2","pkg:npm/hasown@2.0.4"]},{"ref":"pkg:npm/form-data@4.0.6","dependsOn":["pkg:npm/asynckit@0.4.0","pkg:npm/combined-stream@1.0.8","pkg:npm/es-set-tostringtag@2.1.0","pkg:npm/hasown@2.0.4","pkg:npm/mime-types@2.1.35"]},{"ref":"pkg:npm/debug@4.4.3","dependsOn":["pkg:npm/ms@2.1.3"]},{"ref":"pkg:npm/agent-base@6.0.2","dependsOn":["pkg:npm/debug@4.4.3"]},{"ref":"pkg:npm/https-proxy-agent@5.0.1","dependsOn":["pkg:npm/agent-base@6.0.2","pkg:npm/debug@4.4.3"]},{"ref":"pkg:npm/proxy-from-env@2.1.0","dependsOn":[]},{"ref":"pkg:npm/axios@1.18.1","dependsOn":["pkg:npm/follow-redirects@1.16.0","pkg:npm/form-data@4.0.6","pkg:npm/https-proxy-agent@5.0.1","pkg:npm/proxy-from-env@2.1.0"]},{"ref":"pkg:npm/lodash@4.18.1","dependsOn":[]},{"ref":"pkg:npm/moment@2.30.1","dependsOn":[]},{"ref":"pkg:npm/@colors/colors@1.6.0","dependsOn":[]},{"ref":"pkg:npm/color-name@2.1.0","dependsOn":[]},{"ref":"pkg:npm/color-convert@3.1.3","dependsOn":["pkg:npm/color-name@2.1.0"]},{"ref":"pkg:npm/color-string@2.1.4","dependsOn":["pkg:npm/color-name@2.1.0"]},{"ref":"pkg:npm/color@5.0.3","dependsOn":["pkg:npm/color-convert@3.1.3","pkg:npm/color-string@2.1.4"]},{"ref":"pkg:npm/text-hex@1.0.0","dependsOn":[]},{"ref":"pkg:npm/@so-ric/colorspace@1.1.6","dependsOn":["pkg:npm/color@5.0.3","pkg:npm/text-hex@1.0.0"]},{"ref":"pkg:npm/enabled@2.0.0","dependsOn":[]},{"ref":"pkg:npm/kuler@2.0.0","dependsOn":[]},{"ref":"pkg:npm/@dabh/diagnostics@2.0.8","dependsOn":["pkg:npm/@so-ric/colorspace@1.1.6","pkg:npm/enabled@2.0.0","pkg:npm/kuler@2.0.0"]},{"ref":"pkg:npm/async@3.2.6","dependsOn":[]},{"ref":"pkg:npm/is-stream@2.0.1","dependsOn":[]},{"ref":"pkg:npm/@types/triple-beam@1.3.5","dependsOn":[]},{"ref":"pkg:npm/fecha@4.2.3","dependsOn":[]},{"ref":"pkg:npm/safe-stable-stringify@2.5.0","dependsOn":[]},{"ref":"pkg:npm/triple-beam@1.4.1","dependsOn":[]},{"ref":"pkg:npm/logform@2.7.0","dependsOn":["pkg:npm/@colors/colors@1.6.0","pkg:npm/@types/triple-beam@1.3.5","pkg:npm/fecha@4.2.3","pkg:npm/ms@2.1.3","pkg:npm/safe-stable-stringify@2.5.0","pkg:npm/triple-beam@1.4.1"]},{"ref":"pkg:npm/fn.name@1.1.0","dependsOn":[]},{"ref":"pkg:npm/one-time@1.0.0","dependsOn":["pkg:npm/fn.name@1.1.0"]},{"ref":"pkg:npm/string_decoder@1.3.0","dependsOn":["pkg:npm/safe-buffer@5.2.1"]},{"ref":"pkg:npm/util-deprecate@1.0.2","dependsOn":[]},{"ref":"pkg:npm/readable-stream@3.6.2","dependsOn":["pkg:npm/inherits@2.0.4","pkg:npm/string_decoder@1.3.0","pkg:npm/util-deprecate@1.0.2"]},{"ref":"pkg:npm/stack-trace@0.0.10","dependsOn":[]},{"ref":"pkg:npm/winston-transport@4.9.0","dependsOn":["pkg:npm/logform@2.7.0","pkg:npm/readable-stream@3.6.2","pkg:npm/triple-beam@1.4.1"]},{"ref":"pkg:npm/winston@3.19.0","dependsOn":["pkg:npm/@colors/colors@1.6.0","pkg:npm/@dabh/diagnostics@2.0.8","pkg:npm/async@3.2.6","pkg:npm/is-stream@2.0.1","pkg:npm/logform@2.7.0","pkg:npm/one-time@1.0.0","pkg:npm/readable-stream@3.6.2","pkg:npm/safe-stable-stringify@2.5.0","pkg:npm/stack-trace@0.0.10","pkg:npm/triple-beam@1.4.1","pkg:npm/winston-transport@4.9.0"]},{"ref":"pkg:npm/mime-db@1.54.0","dependsOn":[]},{"ref":"pkg:npm/compressible@2.0.18","dependsOn":["pkg:npm/mime-db@1.54.0"]},{"ref":"pkg:npm/negotiator@0.6.4","dependsOn":[]},{"ref":"pkg:npm/compression@1.8.1","dependsOn":["pkg:npm/bytes@3.1.2","pkg:npm/compressible@2.0.18","pkg:npm/debug@2.6.9","pkg:npm/negotiator@0.6.4","pkg:npm/on-headers@1.1.0","pkg:npm/safe-buffer@5.2.1","pkg:npm/vary@1.1.2"]},{"ref":"pkg:npm/normalize-path@3.0.0","dependsOn":[]},{"ref":"pkg:npm/picomatch@2.3.2","dependsOn":[]},{"ref":"pkg:npm/anymatch@3.1.3","dependsOn":["pkg:npm/normalize-path@3.0.0","pkg:npm/picomatch@2.3.2"]},{"ref":"pkg:npm/is-number@7.0.0","dependsOn":[]},{"ref":"pkg:npm/to-regex-range@5.0.1","dependsOn":["pkg:npm/is-number@7.0.0"]},{"ref":"pkg:npm/fill-range@7.1.1","dependsOn":["pkg:npm/to-regex-range@5.0.1"]},{"ref":"pkg:npm/braces@3.0.3","dependsOn":["pkg:npm/fill-range@7.1.1"]},{"ref":"pkg:npm/is-extglob@2.1.1","dependsOn":[]},{"ref":"pkg:npm/is-glob@4.0.3","dependsOn":["pkg:npm/is-extglob@2.1.1"]},{"ref":"pkg:npm/glob-parent@5.1.2","dependsOn":["pkg:npm/is-glob@4.0.3"]},{"ref":"pkg:npm/binary-extensions@2.3.0","dependsOn":[]},{"ref":"pkg:npm/is-binary-path@2.1.0","dependsOn":["pkg:npm/binary-extensions@2.3.0"]},{"ref":"pkg:npm/readdirp@3.6.0","dependsOn":["pkg:npm/picomatch@2.3.2"]},{"ref":"pkg:npm/fsevents@2.3.3","dependsOn":[]},{"ref":"pkg:npm/chokidar@3.6.0","dependsOn":["pkg:npm/anymatch@3.1.3","pkg:npm/braces@3.0.3","pkg:npm/fsevents@2.3.3","pkg:npm/glob-parent@5.1.2","pkg:npm/is-binary-path@2.1.0","pkg:npm/is-glob@4.0.3","pkg:npm/normalize-path@3.0.0","pkg:npm/readdirp@3.6.0"]},{"ref":"pkg:npm/ignore-by-default@1.0.1","dependsOn":[]},{"ref":"pkg:npm/balanced-match@4.0.4","dependsOn":[]},{"ref":"pkg:npm/brace-expansion@5.0.6","dependsOn":["pkg:npm/balanced-match@4.0.4"]},{"ref":"pkg:npm/minimatch@10.2.5","dependsOn":["pkg:npm/brace-expansion@5.0.6"]},{"ref":"pkg:npm/pstree.remy@1.1.8","dependsOn":[]},{"ref":"pkg:npm/semver@7.8.5","dependsOn":[]},{"ref":"pkg:npm/simple-update-notifier@2.0.0","dependsOn":["pkg:npm/semver@7.8.5"]},{"ref":"pkg:npm/has-flag@3.0.0","dependsOn":[]},{"ref":"pkg:npm/supports-color@5.5.0","dependsOn":["pkg:npm/has-flag@3.0.0"]},{"ref":"pkg:npm/touch@3.1.1","dependsOn":[]},{"ref":"pkg:npm/undefsafe@2.0.5","dependsOn":[]},{"ref":"pkg:npm/nodemon@3.1.14","dependsOn":["pkg:npm/chokidar@3.6.0","pkg:npm/debug@4.4.3","pkg:npm/ignore-by-default@1.0.1","pkg:npm/minimatch@10.2.5","pkg:npm/pstree.remy@1.1.8","pkg:npm/semver@7.8.5","pkg:npm/simple-update-notifier@2.0.0","pkg:npm/supports-color@5.5.0","pkg:npm/touch@3.1.1","pkg:npm/undefsafe@2.0.5"]},{"ref":"pkg:npm/@sinclair/typebox@0.27.10","dependsOn":[]},{"ref":"pkg:npm/@jest/schemas@29.6.3","dependsOn":["pkg:npm/@sinclair/typebox@0.27.10"]},{"ref":"pkg:npm/@types/istanbul-lib-coverage@2.0.6","dependsOn":[]},{"ref":"pkg:npm/@types/istanbul-lib-report@3.0.3","dependsOn":["pkg:npm/@types/istanbul-lib-coverage@2.0.6"]},{"ref":"pkg:npm/@types/istanbul-reports@3.0.4","dependsOn":["pkg:npm/@types/istanbul-lib-report@3.0.3"]},{"ref":"pkg:npm/undici-types@8.3.0","dependsOn":[]},{"ref":"pkg:npm/@types/node@26.0.1","dependsOn":["pkg:npm/undici-types@8.3.0"]},{"ref":"pkg:npm/@types/yargs-parser@21.0.3","dependsOn":[]},{"ref":"pkg:npm/@types/yargs@17.0.35","dependsOn":["pkg:npm/@types/yargs-parser@21.0.3"]},{"ref":"pkg:npm/color-name@1.1.4","dependsOn":[]},{"ref":"pkg:npm/color-convert@2.0.1","dependsOn":["pkg:npm/color-name@1.1.4"]},{"ref":"pkg:npm/ansi-styles@4.3.0","dependsOn":["pkg:npm/color-convert@2.0.1"]},{"ref":"pkg:npm/has-flag@4.0.0","dependsOn":[]},{"ref":"pkg:npm/supports-color@7.2.0","dependsOn":["pkg:npm/has-flag@4.0.0"]},{"ref":"pkg:npm/chalk@4.1.2","dependsOn":["pkg:npm/ansi-styles@4.3.0","pkg:npm/supports-color@7.2.0"]},{"ref":"pkg:npm/@jest/types@29.6.3","dependsOn":["pkg:npm/@jest/schemas@29.6.3","pkg:npm/@types/istanbul-lib-coverage@2.0.6","pkg:npm/@types/istanbul-reports@3.0.4","pkg:npm/@types/node@26.0.1","pkg:npm/@types/yargs@17.0.35","pkg:npm/chalk@4.1.2"]},{"ref":"pkg:npm/@babel/helper-validator-identifier@7.29.7","dependsOn":[]},{"ref":"pkg:npm/js-tokens@4.0.0","dependsOn":[]},{"ref":"pkg:npm/picocolors@1.1.1","dependsOn":[]},{"ref":"pkg:npm/@babel/code-frame@7.29.7","dependsOn":["pkg:npm/@babel/helper-validator-identifier@7.29.7","pkg:npm/js-tokens@4.0.0","pkg:npm/picocolors@1.1.1"]},{"ref":"pkg:npm/@types/stack-utils@2.0.3","dependsOn":[]},{"ref":"pkg:npm/graceful-fs@4.2.11","dependsOn":[]},{"ref":"pkg:npm/micromatch@4.0.8","dependsOn":["pkg:npm/braces@3.0.3","pkg:npm/picomatch@2.3.2"]},{"ref":"pkg:npm/ansi-styles@5.2.0","dependsOn":[]},{"ref":"pkg:npm/react-is@18.3.1","dependsOn":[]},{"ref":"pkg:npm/pretty-format@29.7.0","dependsOn":["pkg:npm/@jest/schemas@29.6.3","pkg:npm/ansi-styles@5.2.0","pkg:npm/react-is@18.3.1"]},{"ref":"pkg:npm/slash@3.0.0","dependsOn":[]},{"ref":"pkg:npm/escape-string-regexp@2.0.0","dependsOn":[]},{"ref":"pkg:npm/stack-utils@2.0.6","dependsOn":["pkg:npm/escape-string-regexp@2.0.0"]},{"ref":"pkg:npm/jest-message-util@29.7.0","dependsOn":["pkg:npm/@babel/code-frame@7.29.7","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/stack-utils@2.0.3","pkg:npm/chalk@4.1.2","pkg:npm/graceful-fs@4.2.11","pkg:npm/micromatch@4.0.8","pkg:npm/pretty-format@29.7.0","pkg:npm/slash@3.0.0","pkg:npm/stack-utils@2.0.6"]},{"ref":"pkg:npm/ci-info@3.9.0","dependsOn":[]},{"ref":"pkg:npm/jest-util@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/chalk@4.1.2","pkg:npm/ci-info@3.9.0","pkg:npm/graceful-fs@4.2.11","pkg:npm/picomatch@2.3.2"]},{"ref":"pkg:npm/@jest/console@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/chalk@4.1.2","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/slash@3.0.0"]},{"ref":"pkg:npm/@bcoe/v8-coverage@0.2.3","dependsOn":[]},{"ref":"pkg:npm/collect-v8-coverage@1.0.3","dependsOn":[]},{"ref":"pkg:npm/@jest/test-result@29.7.0","dependsOn":["pkg:npm/@jest/console@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/istanbul-lib-coverage@2.0.6","pkg:npm/collect-v8-coverage@1.0.3"]},{"ref":"pkg:npm/@babel/helper-string-parser@7.29.7","dependsOn":[]},{"ref":"pkg:npm/@babel/types@7.29.7","dependsOn":["pkg:npm/@babel/helper-string-parser@7.29.7","pkg:npm/@babel/helper-validator-identifier@7.29.7"]},{"ref":"pkg:npm/@babel/parser@7.29.7","dependsOn":["pkg:npm/@babel/types@7.29.7"]},{"ref":"pkg:npm/@jridgewell/sourcemap-codec@1.5.5","dependsOn":[]},{"ref":"pkg:npm/@jridgewell/resolve-uri@3.1.2","dependsOn":[]},{"ref":"pkg:npm/@jridgewell/trace-mapping@0.3.31","dependsOn":["pkg:npm/@jridgewell/resolve-uri@3.1.2","pkg:npm/@jridgewell/sourcemap-codec@1.5.5"]},{"ref":"pkg:npm/@jridgewell/gen-mapping@0.3.13","dependsOn":["pkg:npm/@jridgewell/sourcemap-codec@1.5.5","pkg:npm/@jridgewell/trace-mapping@0.3.31"]},{"ref":"pkg:npm/jsesc@3.1.0","dependsOn":[]},{"ref":"pkg:npm/@babel/generator@7.29.7","dependsOn":["pkg:npm/@babel/parser@7.29.7","pkg:npm/@babel/types@7.29.7","pkg:npm/@jridgewell/gen-mapping@0.3.13","pkg:npm/@jridgewell/trace-mapping@0.3.31","pkg:npm/jsesc@3.1.0"]},{"ref":"pkg:npm/@babel/compat-data@7.29.7","dependsOn":[]},{"ref":"pkg:npm/@babel/helper-validator-option@7.29.7","dependsOn":[]},{"ref":"pkg:npm/baseline-browser-mapping@2.10.40","dependsOn":[]},{"ref":"pkg:npm/caniuse-lite@1.0.30001799","dependsOn":[]},{"ref":"pkg:npm/electron-to-chromium@1.5.380","dependsOn":[]},{"ref":"pkg:npm/node-releases@2.0.50","dependsOn":[]},{"ref":"pkg:npm/escalade@3.2.0","dependsOn":[]},{"ref":"pkg:npm/update-browserslist-db@1.2.3","dependsOn":["pkg:npm/browserslist@4.28.4","pkg:npm/escalade@3.2.0","pkg:npm/picocolors@1.1.1"]},{"ref":"pkg:npm/browserslist@4.28.4","dependsOn":["pkg:npm/baseline-browser-mapping@2.10.40","pkg:npm/caniuse-lite@1.0.30001799","pkg:npm/electron-to-chromium@1.5.380","pkg:npm/node-releases@2.0.50","pkg:npm/update-browserslist-db@1.2.3"]},{"ref":"pkg:npm/yallist@3.1.1","dependsOn":[]},{"ref":"pkg:npm/lru-cache@5.1.1","dependsOn":["pkg:npm/yallist@3.1.1"]},{"ref":"pkg:npm/semver@6.3.1","dependsOn":[]},{"ref":"pkg:npm/@babel/helper-compilation-targets@7.29.7","dependsOn":["pkg:npm/@babel/compat-data@7.29.7","pkg:npm/@babel/helper-validator-option@7.29.7","pkg:npm/browserslist@4.28.4","pkg:npm/lru-cache@5.1.1","pkg:npm/semver@6.3.1"]},{"ref":"pkg:npm/@babel/helper-globals@7.29.7","dependsOn":[]},{"ref":"pkg:npm/@babel/template@7.29.7","dependsOn":["pkg:npm/@babel/code-frame@7.29.7","pkg:npm/@babel/parser@7.29.7","pkg:npm/@babel/types@7.29.7"]},{"ref":"pkg:npm/@babel/traverse@7.29.7","dependsOn":["pkg:npm/@babel/code-frame@7.29.7","pkg:npm/@babel/generator@7.29.7","pkg:npm/@babel/helper-globals@7.29.7","pkg:npm/@babel/parser@7.29.7","pkg:npm/@babel/template@7.29.7","pkg:npm/@babel/types@7.29.7","pkg:npm/debug@4.4.3"]},{"ref":"pkg:npm/@babel/helper-module-imports@7.29.7","dependsOn":["pkg:npm/@babel/traverse@7.29.7","pkg:npm/@babel/types@7.29.7"]},{"ref":"pkg:npm/@babel/helper-module-transforms@7.29.7","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-module-imports@7.29.7","pkg:npm/@babel/helper-validator-identifier@7.29.7","pkg:npm/@babel/traverse@7.29.7"]},{"ref":"pkg:npm/@babel/helpers@7.29.7","dependsOn":["pkg:npm/@babel/template@7.29.7","pkg:npm/@babel/types@7.29.7"]},{"ref":"pkg:npm/@jridgewell/remapping@2.3.5","dependsOn":["pkg:npm/@jridgewell/gen-mapping@0.3.13","pkg:npm/@jridgewell/trace-mapping@0.3.31"]},{"ref":"pkg:npm/convert-source-map@2.0.0","dependsOn":[]},{"ref":"pkg:npm/gensync@1.0.0-beta.2","dependsOn":[]},{"ref":"pkg:npm/json5@2.2.3","dependsOn":[]},{"ref":"pkg:npm/@babel/core@7.29.7","dependsOn":["pkg:npm/@babel/code-frame@7.29.7","pkg:npm/@babel/generator@7.29.7","pkg:npm/@babel/helper-compilation-targets@7.29.7","pkg:npm/@babel/helper-module-transforms@7.29.7","pkg:npm/@babel/helpers@7.29.7","pkg:npm/@babel/parser@7.29.7","pkg:npm/@babel/template@7.29.7","pkg:npm/@babel/traverse@7.29.7","pkg:npm/@babel/types@7.29.7","pkg:npm/@jridgewell/remapping@2.3.5","pkg:npm/convert-source-map@2.0.0","pkg:npm/debug@4.4.3","pkg:npm/gensync@1.0.0-beta.2","pkg:npm/json5@2.2.3","pkg:npm/semver@6.3.1"]},{"ref":"pkg:npm/@babel/helper-plugin-utils@7.29.7","dependsOn":[]},{"ref":"pkg:npm/camelcase@5.3.1","dependsOn":[]},{"ref":"pkg:npm/p-try@2.2.0","dependsOn":[]},{"ref":"pkg:npm/p-limit@2.3.0","dependsOn":["pkg:npm/p-try@2.2.0"]},{"ref":"pkg:npm/p-locate@4.1.0","dependsOn":["pkg:npm/p-limit@2.3.0"]},{"ref":"pkg:npm/locate-path@5.0.0","dependsOn":["pkg:npm/p-locate@4.1.0"]},{"ref":"pkg:npm/path-exists@4.0.0","dependsOn":[]},{"ref":"pkg:npm/find-up@4.1.0","dependsOn":["pkg:npm/locate-path@5.0.0","pkg:npm/path-exists@4.0.0"]},{"ref":"pkg:npm/get-package-type@0.1.0","dependsOn":[]},{"ref":"pkg:npm/sprintf-js@1.0.3","dependsOn":[]},{"ref":"pkg:npm/argparse@1.0.10","dependsOn":["pkg:npm/sprintf-js@1.0.3"]},{"ref":"pkg:npm/esprima@4.0.1","dependsOn":[]},{"ref":"pkg:npm/js-yaml@3.15.0","dependsOn":["pkg:npm/argparse@1.0.10","pkg:npm/esprima@4.0.1"]},{"ref":"pkg:npm/resolve-from@5.0.0","dependsOn":[]},{"ref":"pkg:npm/@istanbuljs/load-nyc-config@1.1.0","dependsOn":["pkg:npm/camelcase@5.3.1","pkg:npm/find-up@4.1.0","pkg:npm/get-package-type@0.1.0","pkg:npm/js-yaml@3.15.0","pkg:npm/resolve-from@5.0.0"]},{"ref":"pkg:npm/@istanbuljs/schema@0.1.6","dependsOn":[]},{"ref":"pkg:npm/istanbul-lib-coverage@3.2.2","dependsOn":[]},{"ref":"pkg:npm/istanbul-lib-instrument@5.2.1","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/parser@7.29.7","pkg:npm/@istanbuljs/schema@0.1.6","pkg:npm/istanbul-lib-coverage@3.2.2","pkg:npm/semver@6.3.1"]},{"ref":"pkg:npm/fs.realpath@1.0.0","dependsOn":[]},{"ref":"pkg:npm/wrappy@1.0.2","dependsOn":[]},{"ref":"pkg:npm/once@1.4.0","dependsOn":["pkg:npm/wrappy@1.0.2"]},{"ref":"pkg:npm/inflight@1.0.6","dependsOn":["pkg:npm/once@1.4.0","pkg:npm/wrappy@1.0.2"]},{"ref":"pkg:npm/balanced-match@1.0.2","dependsOn":[]},{"ref":"pkg:npm/concat-map@0.0.1","dependsOn":[]},{"ref":"pkg:npm/brace-expansion@1.1.15","dependsOn":["pkg:npm/balanced-match@1.0.2","pkg:npm/concat-map@0.0.1"]},{"ref":"pkg:npm/minimatch@3.1.5","dependsOn":["pkg:npm/brace-expansion@1.1.15"]},{"ref":"pkg:npm/path-is-absolute@1.0.1","dependsOn":[]},{"ref":"pkg:npm/glob@7.2.3","dependsOn":["pkg:npm/fs.realpath@1.0.0","pkg:npm/inflight@1.0.6","pkg:npm/inherits@2.0.4","pkg:npm/minimatch@3.1.5","pkg:npm/once@1.4.0","pkg:npm/path-is-absolute@1.0.1"]},{"ref":"pkg:npm/test-exclude@6.0.0","dependsOn":["pkg:npm/@istanbuljs/schema@0.1.6","pkg:npm/glob@7.2.3","pkg:npm/minimatch@3.1.5"]},{"ref":"pkg:npm/babel-plugin-istanbul@6.1.1","dependsOn":["pkg:npm/@babel/helper-plugin-utils@7.29.7","pkg:npm/@istanbuljs/load-nyc-config@1.1.0","pkg:npm/@istanbuljs/schema@0.1.6","pkg:npm/istanbul-lib-instrument@5.2.1","pkg:npm/test-exclude@6.0.0"]},{"ref":"pkg:npm/fast-json-stable-stringify@2.1.0","dependsOn":[]},{"ref":"pkg:npm/@types/graceful-fs@4.1.9","dependsOn":["pkg:npm/@types/node@26.0.1"]},{"ref":"pkg:npm/node-int64@0.4.0","dependsOn":[]},{"ref":"pkg:npm/bser@2.1.1","dependsOn":["pkg:npm/node-int64@0.4.0"]},{"ref":"pkg:npm/fb-watchman@2.0.2","dependsOn":["pkg:npm/bser@2.1.1"]},{"ref":"pkg:npm/jest-regex-util@29.6.3","dependsOn":[]},{"ref":"pkg:npm/merge-stream@2.0.0","dependsOn":[]},{"ref":"pkg:npm/supports-color@8.1.1","dependsOn":["pkg:npm/has-flag@4.0.0"]},{"ref":"pkg:npm/jest-worker@29.7.0","dependsOn":["pkg:npm/@types/node@26.0.1","pkg:npm/jest-util@29.7.0","pkg:npm/merge-stream@2.0.0","pkg:npm/supports-color@8.1.1"]},{"ref":"pkg:npm/tmpl@1.0.5","dependsOn":[]},{"ref":"pkg:npm/makeerror@1.0.12","dependsOn":["pkg:npm/tmpl@1.0.5"]},{"ref":"pkg:npm/walker@1.0.8","dependsOn":["pkg:npm/makeerror@1.0.12"]},{"ref":"pkg:npm/jest-haste-map@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/@types/graceful-fs@4.1.9","pkg:npm/@types/node@26.0.1","pkg:npm/anymatch@3.1.3","pkg:npm/fb-watchman@2.0.2","pkg:npm/fsevents@2.3.3","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-regex-util@29.6.3","pkg:npm/jest-util@29.7.0","pkg:npm/jest-worker@29.7.0","pkg:npm/micromatch@4.0.8","pkg:npm/walker@1.0.8"]},{"ref":"pkg:npm/pirates@4.0.7","dependsOn":[]},{"ref":"pkg:npm/imurmurhash@0.1.4","dependsOn":[]},{"ref":"pkg:npm/signal-exit@3.0.7","dependsOn":[]},{"ref":"pkg:npm/write-file-atomic@4.0.2","dependsOn":["pkg:npm/imurmurhash@0.1.4","pkg:npm/signal-exit@3.0.7"]},{"ref":"pkg:npm/@jest/transform@29.7.0","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@jest/types@29.6.3","pkg:npm/@jridgewell/trace-mapping@0.3.31","pkg:npm/babel-plugin-istanbul@6.1.1","pkg:npm/chalk@4.1.2","pkg:npm/convert-source-map@2.0.0","pkg:npm/fast-json-stable-stringify@2.1.0","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-haste-map@29.7.0","pkg:npm/jest-regex-util@29.6.3","pkg:npm/jest-util@29.7.0","pkg:npm/micromatch@4.0.8","pkg:npm/pirates@4.0.7","pkg:npm/slash@3.0.0","pkg:npm/write-file-atomic@4.0.2"]},{"ref":"pkg:npm/exit@0.1.2","dependsOn":[]},{"ref":"pkg:npm/istanbul-lib-instrument@6.0.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/parser@7.29.7","pkg:npm/@istanbuljs/schema@0.1.6","pkg:npm/istanbul-lib-coverage@3.2.2","pkg:npm/semver@7.8.5"]},{"ref":"pkg:npm/make-dir@4.0.0","dependsOn":["pkg:npm/semver@7.8.5"]},{"ref":"pkg:npm/istanbul-lib-report@3.0.1","dependsOn":["pkg:npm/istanbul-lib-coverage@3.2.2","pkg:npm/make-dir@4.0.0","pkg:npm/supports-color@7.2.0"]},{"ref":"pkg:npm/source-map@0.6.1","dependsOn":[]},{"ref":"pkg:npm/istanbul-lib-source-maps@4.0.1","dependsOn":["pkg:npm/debug@4.4.3","pkg:npm/istanbul-lib-coverage@3.2.2","pkg:npm/source-map@0.6.1"]},{"ref":"pkg:npm/html-escaper@2.0.2","dependsOn":[]},{"ref":"pkg:npm/istanbul-reports@3.2.0","dependsOn":["pkg:npm/html-escaper@2.0.2","pkg:npm/istanbul-lib-report@3.0.1"]},{"ref":"pkg:npm/char-regex@1.0.2","dependsOn":[]},{"ref":"pkg:npm/ansi-regex@5.0.1","dependsOn":[]},{"ref":"pkg:npm/strip-ansi@6.0.1","dependsOn":["pkg:npm/ansi-regex@5.0.1"]},{"ref":"pkg:npm/string-length@4.0.2","dependsOn":["pkg:npm/char-regex@1.0.2","pkg:npm/strip-ansi@6.0.1"]},{"ref":"pkg:npm/v8-to-istanbul@9.3.0","dependsOn":["pkg:npm/@jridgewell/trace-mapping@0.3.31","pkg:npm/@types/istanbul-lib-coverage@2.0.6","pkg:npm/convert-source-map@2.0.0"]},{"ref":"pkg:npm/@jest/reporters@29.7.0","dependsOn":["pkg:npm/@bcoe/v8-coverage@0.2.3","pkg:npm/@jest/console@29.7.0","pkg:npm/@jest/test-result@29.7.0","pkg:npm/@jest/transform@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@jridgewell/trace-mapping@0.3.31","pkg:npm/@types/node@26.0.1","pkg:npm/chalk@4.1.2","pkg:npm/collect-v8-coverage@1.0.3","pkg:npm/exit@0.1.2","pkg:npm/glob@7.2.3","pkg:npm/graceful-fs@4.2.11","pkg:npm/istanbul-lib-coverage@3.2.2","pkg:npm/istanbul-lib-instrument@6.0.3","pkg:npm/istanbul-lib-report@3.0.1","pkg:npm/istanbul-lib-source-maps@4.0.1","pkg:npm/istanbul-reports@3.2.0","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/jest-worker@29.7.0","pkg:npm/slash@3.0.0","pkg:npm/string-length@4.0.2","pkg:npm/strip-ansi@6.0.1","pkg:npm/v8-to-istanbul@9.3.0"]},{"ref":"pkg:npm/type-fest@0.21.3","dependsOn":[]},{"ref":"pkg:npm/ansi-escapes@4.3.2","dependsOn":["pkg:npm/type-fest@0.21.3"]},{"ref":"pkg:npm/path-key@3.1.1","dependsOn":[]},{"ref":"pkg:npm/shebang-regex@3.0.0","dependsOn":[]},{"ref":"pkg:npm/shebang-command@2.0.0","dependsOn":["pkg:npm/shebang-regex@3.0.0"]},{"ref":"pkg:npm/isexe@2.0.0","dependsOn":[]},{"ref":"pkg:npm/which@2.0.2","dependsOn":["pkg:npm/isexe@2.0.0"]},{"ref":"pkg:npm/cross-spawn@7.0.6","dependsOn":["pkg:npm/path-key@3.1.1","pkg:npm/shebang-command@2.0.0","pkg:npm/which@2.0.2"]},{"ref":"pkg:npm/get-stream@6.0.1","dependsOn":[]},{"ref":"pkg:npm/human-signals@2.1.0","dependsOn":[]},{"ref":"pkg:npm/npm-run-path@4.0.1","dependsOn":["pkg:npm/path-key@3.1.1"]},{"ref":"pkg:npm/mimic-fn@2.1.0","dependsOn":[]},{"ref":"pkg:npm/onetime@5.1.2","dependsOn":["pkg:npm/mimic-fn@2.1.0"]},{"ref":"pkg:npm/strip-final-newline@2.0.0","dependsOn":[]},{"ref":"pkg:npm/execa@5.1.1","dependsOn":["pkg:npm/cross-spawn@7.0.6","pkg:npm/get-stream@6.0.1","pkg:npm/human-signals@2.1.0","pkg:npm/is-stream@2.0.1","pkg:npm/merge-stream@2.0.0","pkg:npm/npm-run-path@4.0.1","pkg:npm/onetime@5.1.2","pkg:npm/signal-exit@3.0.7","pkg:npm/strip-final-newline@2.0.0"]},{"ref":"pkg:npm/yocto-queue@0.1.0","dependsOn":[]},{"ref":"pkg:npm/p-limit@3.1.0","dependsOn":["pkg:npm/yocto-queue@0.1.0"]},{"ref":"pkg:npm/jest-changed-files@29.7.0","dependsOn":["pkg:npm/execa@5.1.1","pkg:npm/jest-util@29.7.0","pkg:npm/p-limit@3.1.0"]},{"ref":"pkg:npm/@jest/test-sequencer@29.7.0","dependsOn":["pkg:npm/@jest/test-result@29.7.0","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-haste-map@29.7.0","pkg:npm/slash@3.0.0"]},{"ref":"pkg:npm/@types/babel__generator@7.27.0","dependsOn":["pkg:npm/@babel/types@7.29.7"]},{"ref":"pkg:npm/@types/babel__template@7.4.4","dependsOn":["pkg:npm/@babel/parser@7.29.7","pkg:npm/@babel/types@7.29.7"]},{"ref":"pkg:npm/@types/babel__traverse@7.28.0","dependsOn":["pkg:npm/@babel/types@7.29.7"]},{"ref":"pkg:npm/@types/babel__core@7.20.5","dependsOn":["pkg:npm/@babel/parser@7.29.7","pkg:npm/@babel/types@7.29.7","pkg:npm/@types/babel__generator@7.27.0","pkg:npm/@types/babel__template@7.4.4","pkg:npm/@types/babel__traverse@7.28.0"]},{"ref":"pkg:npm/babel-plugin-jest-hoist@29.6.3","dependsOn":["pkg:npm/@babel/template@7.29.7","pkg:npm/@babel/types@7.29.7","pkg:npm/@types/babel__core@7.20.5","pkg:npm/@types/babel__traverse@7.28.0"]},{"ref":"pkg:npm/@babel/plugin-syntax-async-generators@7.8.4","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-bigint@7.8.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-class-properties@7.12.13","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-class-static-block@7.14.5","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-import-attributes@7.29.7","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-import-meta@7.10.4","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-json-strings@7.8.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-logical-assignment-operators@7.10.4","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-nullish-coalescing-operator@7.8.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-numeric-separator@7.10.4","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-object-rest-spread@7.8.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-optional-catch-binding@7.8.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-optional-chaining@7.8.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-private-property-in-object@7.14.5","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-top-level-await@7.14.5","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/babel-preset-current-node-syntax@1.2.0","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/plugin-syntax-async-generators@7.8.4","pkg:npm/@babel/plugin-syntax-bigint@7.8.3","pkg:npm/@babel/plugin-syntax-class-properties@7.12.13","pkg:npm/@babel/plugin-syntax-class-static-block@7.14.5","pkg:npm/@babel/plugin-syntax-import-attributes@7.29.7","pkg:npm/@babel/plugin-syntax-import-meta@7.10.4","pkg:npm/@babel/plugin-syntax-json-strings@7.8.3","pkg:npm/@babel/plugin-syntax-logical-assignment-operators@7.10.4","pkg:npm/@babel/plugin-syntax-nullish-coalescing-operator@7.8.3","pkg:npm/@babel/plugin-syntax-numeric-separator@7.10.4","pkg:npm/@babel/plugin-syntax-object-rest-spread@7.8.3","pkg:npm/@babel/plugin-syntax-optional-catch-binding@7.8.3","pkg:npm/@babel/plugin-syntax-optional-chaining@7.8.3","pkg:npm/@babel/plugin-syntax-private-property-in-object@7.14.5","pkg:npm/@babel/plugin-syntax-top-level-await@7.14.5"]},{"ref":"pkg:npm/babel-preset-jest@29.6.3","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/babel-plugin-jest-hoist@29.6.3","pkg:npm/babel-preset-current-node-syntax@1.2.0"]},{"ref":"pkg:npm/babel-jest@29.7.0","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@jest/transform@29.7.0","pkg:npm/@types/babel__core@7.20.5","pkg:npm/babel-plugin-istanbul@6.1.1","pkg:npm/babel-preset-jest@29.6.3","pkg:npm/chalk@4.1.2","pkg:npm/graceful-fs@4.2.11","pkg:npm/slash@3.0.0"]},{"ref":"pkg:npm/deepmerge@4.3.1","dependsOn":[]},{"ref":"pkg:npm/type-detect@4.0.8","dependsOn":[]},{"ref":"pkg:npm/@sinonjs/commons@3.0.1","dependsOn":["pkg:npm/type-detect@4.0.8"]},{"ref":"pkg:npm/@sinonjs/fake-timers@10.3.0","dependsOn":["pkg:npm/@sinonjs/commons@3.0.1"]},{"ref":"pkg:npm/jest-mock@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/jest-util@29.7.0"]},{"ref":"pkg:npm/@jest/fake-timers@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/@sinonjs/fake-timers@10.3.0","pkg:npm/@types/node@26.0.1","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-mock@29.7.0","pkg:npm/jest-util@29.7.0"]},{"ref":"pkg:npm/@jest/environment@29.7.0","dependsOn":["pkg:npm/@jest/fake-timers@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/jest-mock@29.7.0"]},{"ref":"pkg:npm/jest-get-type@29.6.3","dependsOn":[]},{"ref":"pkg:npm/@jest/expect-utils@29.7.0","dependsOn":["pkg:npm/jest-get-type@29.6.3"]},{"ref":"pkg:npm/diff-sequences@29.6.3","dependsOn":[]},{"ref":"pkg:npm/jest-diff@29.7.0","dependsOn":["pkg:npm/chalk@4.1.2","pkg:npm/diff-sequences@29.6.3","pkg:npm/jest-get-type@29.6.3","pkg:npm/pretty-format@29.7.0"]},{"ref":"pkg:npm/jest-matcher-utils@29.7.0","dependsOn":["pkg:npm/chalk@4.1.2","pkg:npm/jest-diff@29.7.0","pkg:npm/jest-get-type@29.6.3","pkg:npm/pretty-format@29.7.0"]},{"ref":"pkg:npm/expect@29.7.0","dependsOn":["pkg:npm/@jest/expect-utils@29.7.0","pkg:npm/jest-get-type@29.6.3","pkg:npm/jest-matcher-utils@29.7.0","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-util@29.7.0"]},{"ref":"pkg:npm/@babel/plugin-syntax-jsx@7.29.7","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/@babel/plugin-syntax-typescript@7.29.7","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/helper-plugin-utils@7.29.7"]},{"ref":"pkg:npm/natural-compare@1.4.0","dependsOn":[]},{"ref":"pkg:npm/jest-snapshot@29.7.0","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@babel/generator@7.29.7","pkg:npm/@babel/plugin-syntax-jsx@7.29.7","pkg:npm/@babel/plugin-syntax-typescript@7.29.7","pkg:npm/@babel/types@7.29.7","pkg:npm/@jest/expect-utils@29.7.0","pkg:npm/@jest/transform@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/babel-preset-current-node-syntax@1.2.0","pkg:npm/chalk@4.1.2","pkg:npm/expect@29.7.0","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-diff@29.7.0","pkg:npm/jest-get-type@29.6.3","pkg:npm/jest-matcher-utils@29.7.0","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/natural-compare@1.4.0","pkg:npm/pretty-format@29.7.0","pkg:npm/semver@7.8.5"]},{"ref":"pkg:npm/@jest/expect@29.7.0","dependsOn":["pkg:npm/expect@29.7.0","pkg:npm/jest-snapshot@29.7.0"]},{"ref":"pkg:npm/co@4.6.0","dependsOn":[]},{"ref":"pkg:npm/dedent@1.7.2","dependsOn":[]},{"ref":"pkg:npm/is-generator-fn@2.1.0","dependsOn":[]},{"ref":"pkg:npm/jest-each@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/chalk@4.1.2","pkg:npm/jest-get-type@29.6.3","pkg:npm/jest-util@29.7.0","pkg:npm/pretty-format@29.7.0"]},{"ref":"pkg:npm/@jest/globals@29.7.0","dependsOn":["pkg:npm/@jest/environment@29.7.0","pkg:npm/@jest/expect@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/jest-mock@29.7.0"]},{"ref":"pkg:npm/callsites@3.1.0","dependsOn":[]},{"ref":"pkg:npm/@jest/source-map@29.6.3","dependsOn":["pkg:npm/@jridgewell/trace-mapping@0.3.31","pkg:npm/callsites@3.1.0","pkg:npm/graceful-fs@4.2.11"]},{"ref":"pkg:npm/cjs-module-lexer@1.4.3","dependsOn":[]},{"ref":"pkg:npm/jest-pnp-resolver@1.2.3","dependsOn":["pkg:npm/jest-resolve@29.7.0"]},{"ref":"pkg:npm/camelcase@6.3.0","dependsOn":[]},{"ref":"pkg:npm/leven@3.1.0","dependsOn":[]},{"ref":"pkg:npm/jest-validate@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/camelcase@6.3.0","pkg:npm/chalk@4.1.2","pkg:npm/jest-get-type@29.6.3","pkg:npm/leven@3.1.0","pkg:npm/pretty-format@29.7.0"]},{"ref":"pkg:npm/is-core-module@2.16.2","dependsOn":["pkg:npm/hasown@2.0.4"]},{"ref":"pkg:npm/path-parse@1.0.7","dependsOn":[]},{"ref":"pkg:npm/supports-preserve-symlinks-flag@1.0.0","dependsOn":[]},{"ref":"pkg:npm/resolve@1.22.12","dependsOn":["pkg:npm/es-errors@1.3.0","pkg:npm/is-core-module@2.16.2","pkg:npm/path-parse@1.0.7","pkg:npm/supports-preserve-symlinks-flag@1.0.0"]},{"ref":"pkg:npm/resolve.exports@2.0.3","dependsOn":[]},{"ref":"pkg:npm/jest-resolve@29.7.0","dependsOn":["pkg:npm/chalk@4.1.2","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-haste-map@29.7.0","pkg:npm/jest-pnp-resolver@1.2.3","pkg:npm/jest-util@29.7.0","pkg:npm/jest-validate@29.7.0","pkg:npm/resolve.exports@2.0.3","pkg:npm/resolve@1.22.12","pkg:npm/slash@3.0.0"]},{"ref":"pkg:npm/strip-bom@4.0.0","dependsOn":[]},{"ref":"pkg:npm/jest-runtime@29.7.0","dependsOn":["pkg:npm/@jest/environment@29.7.0","pkg:npm/@jest/fake-timers@29.7.0","pkg:npm/@jest/globals@29.7.0","pkg:npm/@jest/source-map@29.6.3","pkg:npm/@jest/test-result@29.7.0","pkg:npm/@jest/transform@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/chalk@4.1.2","pkg:npm/cjs-module-lexer@1.4.3","pkg:npm/collect-v8-coverage@1.0.3","pkg:npm/glob@7.2.3","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-haste-map@29.7.0","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-mock@29.7.0","pkg:npm/jest-regex-util@29.6.3","pkg:npm/jest-resolve@29.7.0","pkg:npm/jest-snapshot@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/slash@3.0.0","pkg:npm/strip-bom@4.0.0"]},{"ref":"pkg:npm/pure-rand@6.1.0","dependsOn":[]},{"ref":"pkg:npm/jest-circus@29.7.0","dependsOn":["pkg:npm/@jest/environment@29.7.0","pkg:npm/@jest/expect@29.7.0","pkg:npm/@jest/test-result@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/chalk@4.1.2","pkg:npm/co@4.6.0","pkg:npm/dedent@1.7.2","pkg:npm/is-generator-fn@2.1.0","pkg:npm/jest-each@29.7.0","pkg:npm/jest-matcher-utils@29.7.0","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-runtime@29.7.0","pkg:npm/jest-snapshot@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/p-limit@3.1.0","pkg:npm/pretty-format@29.7.0","pkg:npm/pure-rand@6.1.0","pkg:npm/slash@3.0.0","pkg:npm/stack-utils@2.0.6"]},{"ref":"pkg:npm/jest-environment-node@29.7.0","dependsOn":["pkg:npm/@jest/environment@29.7.0","pkg:npm/@jest/fake-timers@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/jest-mock@29.7.0","pkg:npm/jest-util@29.7.0"]},{"ref":"pkg:npm/emittery@0.13.1","dependsOn":[]},{"ref":"pkg:npm/detect-newline@3.1.0","dependsOn":[]},{"ref":"pkg:npm/jest-docblock@29.7.0","dependsOn":["pkg:npm/detect-newline@3.1.0"]},{"ref":"pkg:npm/jest-leak-detector@29.7.0","dependsOn":["pkg:npm/jest-get-type@29.6.3","pkg:npm/pretty-format@29.7.0"]},{"ref":"pkg:npm/jest-watcher@29.7.0","dependsOn":["pkg:npm/@jest/test-result@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/ansi-escapes@4.3.2","pkg:npm/chalk@4.1.2","pkg:npm/emittery@0.13.1","pkg:npm/jest-util@29.7.0","pkg:npm/string-length@4.0.2"]},{"ref":"pkg:npm/buffer-from@1.1.2","dependsOn":[]},{"ref":"pkg:npm/source-map-support@0.5.13","dependsOn":["pkg:npm/buffer-from@1.1.2","pkg:npm/source-map@0.6.1"]},{"ref":"pkg:npm/jest-runner@29.7.0","dependsOn":["pkg:npm/@jest/console@29.7.0","pkg:npm/@jest/environment@29.7.0","pkg:npm/@jest/test-result@29.7.0","pkg:npm/@jest/transform@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/chalk@4.1.2","pkg:npm/emittery@0.13.1","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-docblock@29.7.0","pkg:npm/jest-environment-node@29.7.0","pkg:npm/jest-haste-map@29.7.0","pkg:npm/jest-leak-detector@29.7.0","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-resolve@29.7.0","pkg:npm/jest-runtime@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/jest-watcher@29.7.0","pkg:npm/jest-worker@29.7.0","pkg:npm/p-limit@3.1.0","pkg:npm/source-map-support@0.5.13"]},{"ref":"pkg:npm/is-arrayish@0.2.1","dependsOn":[]},{"ref":"pkg:npm/error-ex@1.3.4","dependsOn":["pkg:npm/is-arrayish@0.2.1"]},{"ref":"pkg:npm/json-parse-even-better-errors@2.3.1","dependsOn":[]},{"ref":"pkg:npm/lines-and-columns@1.2.4","dependsOn":[]},{"ref":"pkg:npm/parse-json@5.2.0","dependsOn":["pkg:npm/@babel/code-frame@7.29.7","pkg:npm/error-ex@1.3.4","pkg:npm/json-parse-even-better-errors@2.3.1","pkg:npm/lines-and-columns@1.2.4"]},{"ref":"pkg:npm/strip-json-comments@3.1.1","dependsOn":[]},{"ref":"pkg:npm/jest-config@29.7.0","dependsOn":["pkg:npm/@babel/core@7.29.7","pkg:npm/@jest/test-sequencer@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/babel-jest@29.7.0","pkg:npm/chalk@4.1.2","pkg:npm/ci-info@3.9.0","pkg:npm/deepmerge@4.3.1","pkg:npm/glob@7.2.3","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-circus@29.7.0","pkg:npm/jest-environment-node@29.7.0","pkg:npm/jest-get-type@29.6.3","pkg:npm/jest-regex-util@29.6.3","pkg:npm/jest-resolve@29.7.0","pkg:npm/jest-runner@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/jest-validate@29.7.0","pkg:npm/micromatch@4.0.8","pkg:npm/parse-json@5.2.0","pkg:npm/pretty-format@29.7.0","pkg:npm/slash@3.0.0","pkg:npm/strip-json-comments@3.1.1"]},{"ref":"pkg:npm/jest-resolve-dependencies@29.7.0","dependsOn":["pkg:npm/jest-regex-util@29.6.3","pkg:npm/jest-snapshot@29.7.0"]},{"ref":"pkg:npm/@jest/core@29.7.0","dependsOn":["pkg:npm/@jest/console@29.7.0","pkg:npm/@jest/reporters@29.7.0","pkg:npm/@jest/test-result@29.7.0","pkg:npm/@jest/transform@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/@types/node@26.0.1","pkg:npm/ansi-escapes@4.3.2","pkg:npm/chalk@4.1.2","pkg:npm/ci-info@3.9.0","pkg:npm/exit@0.1.2","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-changed-files@29.7.0","pkg:npm/jest-config@29.7.0","pkg:npm/jest-haste-map@29.7.0","pkg:npm/jest-message-util@29.7.0","pkg:npm/jest-regex-util@29.6.3","pkg:npm/jest-resolve-dependencies@29.7.0","pkg:npm/jest-resolve@29.7.0","pkg:npm/jest-runner@29.7.0","pkg:npm/jest-runtime@29.7.0","pkg:npm/jest-snapshot@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/jest-validate@29.7.0","pkg:npm/jest-watcher@29.7.0","pkg:npm/micromatch@4.0.8","pkg:npm/pretty-format@29.7.0","pkg:npm/slash@3.0.0","pkg:npm/strip-ansi@6.0.1"]},{"ref":"pkg:npm/pkg-dir@4.2.0","dependsOn":["pkg:npm/find-up@4.1.0"]},{"ref":"pkg:npm/resolve-cwd@3.0.0","dependsOn":["pkg:npm/resolve-from@5.0.0"]},{"ref":"pkg:npm/import-local@3.2.0","dependsOn":["pkg:npm/pkg-dir@4.2.0","pkg:npm/resolve-cwd@3.0.0"]},{"ref":"pkg:npm/kleur@3.0.3","dependsOn":[]},{"ref":"pkg:npm/sisteransi@1.0.5","dependsOn":[]},{"ref":"pkg:npm/prompts@2.4.2","dependsOn":["pkg:npm/kleur@3.0.3","pkg:npm/sisteransi@1.0.5"]},{"ref":"pkg:npm/create-jest@29.7.0","dependsOn":["pkg:npm/@jest/types@29.6.3","pkg:npm/chalk@4.1.2","pkg:npm/exit@0.1.2","pkg:npm/graceful-fs@4.2.11","pkg:npm/jest-config@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/prompts@2.4.2"]},{"ref":"pkg:npm/emoji-regex@8.0.0","dependsOn":[]},{"ref":"pkg:npm/is-fullwidth-code-point@3.0.0","dependsOn":[]},{"ref":"pkg:npm/string-width@4.2.3","dependsOn":["pkg:npm/emoji-regex@8.0.0","pkg:npm/is-fullwidth-code-point@3.0.0","pkg:npm/strip-ansi@6.0.1"]},{"ref":"pkg:npm/wrap-ansi@7.0.0","dependsOn":["pkg:npm/ansi-styles@4.3.0","pkg:npm/string-width@4.2.3","pkg:npm/strip-ansi@6.0.1"]},{"ref":"pkg:npm/cliui@8.0.1","dependsOn":["pkg:npm/string-width@4.2.3","pkg:npm/strip-ansi@6.0.1","pkg:npm/wrap-ansi@7.0.0"]},{"ref":"pkg:npm/get-caller-file@2.0.5","dependsOn":[]},{"ref":"pkg:npm/require-directory@2.1.1","dependsOn":[]},{"ref":"pkg:npm/y18n@5.0.8","dependsOn":[]},{"ref":"pkg:npm/yargs-parser@21.1.1","dependsOn":[]},{"ref":"pkg:npm/yargs@17.7.3","dependsOn":["pkg:npm/cliui@8.0.1","pkg:npm/escalade@3.2.0","pkg:npm/get-caller-file@2.0.5","pkg:npm/require-directory@2.1.1","pkg:npm/string-width@4.2.3","pkg:npm/y18n@5.0.8","pkg:npm/yargs-parser@21.1.1"]},{"ref":"pkg:npm/jest-cli@29.7.0","dependsOn":["pkg:npm/@jest/core@29.7.0","pkg:npm/@jest/test-result@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/chalk@4.1.2","pkg:npm/create-jest@29.7.0","pkg:npm/exit@0.1.2","pkg:npm/import-local@3.2.0","pkg:npm/jest-config@29.7.0","pkg:npm/jest-util@29.7.0","pkg:npm/jest-validate@29.7.0","pkg:npm/yargs@17.7.3"]},{"ref":"pkg:npm/jest@29.7.0","dependsOn":["pkg:npm/@jest/core@29.7.0","pkg:npm/@jest/types@29.6.3","pkg:npm/import-local@3.2.0","pkg:npm/jest-cli@29.7.0"]},{"ref":"pkg:npm/component-emitter@1.3.1","dependsOn":[]},{"ref":"pkg:npm/cookiejar@2.1.4","dependsOn":[]},{"ref":"pkg:npm/fast-safe-stringify@2.1.1","dependsOn":[]},{"ref":"pkg:npm/@noble/hashes@1.8.0","dependsOn":[]},{"ref":"pkg:npm/@paralleldrive/cuid2@2.3.1","dependsOn":["pkg:npm/@noble/hashes@1.8.0"]},{"ref":"pkg:npm/asap@2.0.6","dependsOn":[]},{"ref":"pkg:npm/dezalgo@1.0.4","dependsOn":["pkg:npm/asap@2.0.6","pkg:npm/wrappy@1.0.2"]},{"ref":"pkg:npm/formidable@2.1.5","dependsOn":["pkg:npm/@paralleldrive/cuid2@2.3.1","pkg:npm/dezalgo@1.0.4","pkg:npm/once@1.4.0","pkg:npm/qs@6.15.3"]},{"ref":"pkg:npm/mime@2.6.0","dependsOn":[]},{"ref":"pkg:npm/superagent@8.1.2","dependsOn":["pkg:npm/component-emitter@1.3.1","pkg:npm/cookiejar@2.1.4","pkg:npm/debug@4.4.3","pkg:npm/fast-safe-stringify@2.1.1","pkg:npm/form-data@4.0.6","pkg:npm/formidable@2.1.5","pkg:npm/methods@1.1.2","pkg:npm/mime@2.6.0","pkg:npm/qs@6.15.3","pkg:npm/semver@7.8.5"]},{"ref":"pkg:npm/supertest@6.3.4","dependsOn":["pkg:npm/methods@1.1.2","pkg:npm/superagent@8.1.2"]},{"ref":"pkg:npm/eslint-visitor-keys@3.4.3","dependsOn":[]},{"ref":"pkg:npm/@eslint-community/eslint-utils@4.9.1","dependsOn":["pkg:npm/eslint-visitor-keys@3.4.3","pkg:npm/eslint@8.57.1"]},{"ref":"pkg:npm/@eslint-community/regexpp@4.12.2","dependsOn":[]},{"ref":"pkg:npm/fast-deep-equal@3.1.3","dependsOn":[]},{"ref":"pkg:npm/json-schema-traverse@0.4.1","dependsOn":[]},{"ref":"pkg:npm/punycode@2.3.1","dependsOn":[]},{"ref":"pkg:npm/uri-js@4.4.1","dependsOn":["pkg:npm/punycode@2.3.1"]},{"ref":"pkg:npm/ajv@6.15.0","dependsOn":["pkg:npm/fast-deep-equal@3.1.3","pkg:npm/fast-json-stable-stringify@2.1.0","pkg:npm/json-schema-traverse@0.4.1","pkg:npm/uri-js@4.4.1"]},{"ref":"pkg:npm/acorn@8.17.0","dependsOn":[]},{"ref":"pkg:npm/acorn-jsx@5.3.2","dependsOn":["pkg:npm/acorn@8.17.0"]},{"ref":"pkg:npm/espree@9.6.1","dependsOn":["pkg:npm/acorn-jsx@5.3.2","pkg:npm/acorn@8.17.0","pkg:npm/eslint-visitor-keys@3.4.3"]},{"ref":"pkg:npm/type-fest@0.20.2","dependsOn":[]},{"ref":"pkg:npm/globals@13.24.0","dependsOn":["pkg:npm/type-fest@0.20.2"]},{"ref":"pkg:npm/ignore@5.3.2","dependsOn":[]},{"ref":"pkg:npm/parent-module@1.0.1","dependsOn":["pkg:npm/callsites@3.1.0"]},{"ref":"pkg:npm/resolve-from@4.0.0","dependsOn":[]},{"ref":"pkg:npm/import-fresh@3.3.1","dependsOn":["pkg:npm/parent-module@1.0.1","pkg:npm/resolve-from@4.0.0"]},{"ref":"pkg:npm/argparse@2.0.1","dependsOn":[]},{"ref":"pkg:npm/js-yaml@4.3.0","dependsOn":["pkg:npm/argparse@2.0.1"]},{"ref":"pkg:npm/@eslint/eslintrc@2.1.4","dependsOn":["pkg:npm/ajv@6.15.0","pkg:npm/debug@4.4.3","pkg:npm/espree@9.6.1","pkg:npm/globals@13.24.0","pkg:npm/ignore@5.3.2","pkg:npm/import-fresh@3.3.1","pkg:npm/js-yaml@4.3.0","pkg:npm/minimatch@3.1.5","pkg:npm/strip-json-comments@3.1.1"]},{"ref":"pkg:npm/@eslint/js@8.57.1","dependsOn":[]},{"ref":"pkg:npm/@humanwhocodes/object-schema@2.0.3","dependsOn":[]},{"ref":"pkg:npm/@humanwhocodes/config-array@0.13.0","dependsOn":["pkg:npm/@humanwhocodes/object-schema@2.0.3","pkg:npm/debug@4.4.3","pkg:npm/minimatch@3.1.5"]},{"ref":"pkg:npm/@humanwhocodes/module-importer@1.0.1","dependsOn":[]},{"ref":"pkg:npm/@nodelib/fs.stat@2.0.5","dependsOn":[]},{"ref":"pkg:npm/queue-microtask@1.2.3","dependsOn":[]},{"ref":"pkg:npm/run-parallel@1.2.0","dependsOn":["pkg:npm/queue-microtask@1.2.3"]},{"ref":"pkg:npm/@nodelib/fs.scandir@2.1.5","dependsOn":["pkg:npm/@nodelib/fs.stat@2.0.5","pkg:npm/run-parallel@1.2.0"]},{"ref":"pkg:npm/reusify@1.1.0","dependsOn":[]},{"ref":"pkg:npm/fastq@1.20.1","dependsOn":["pkg:npm/reusify@1.1.0"]},{"ref":"pkg:npm/@nodelib/fs.walk@1.2.8","dependsOn":["pkg:npm/@nodelib/fs.scandir@2.1.5","pkg:npm/fastq@1.20.1"]},{"ref":"pkg:npm/@ungap/structured-clone@1.3.2","dependsOn":[]},{"ref":"pkg:npm/esutils@2.0.3","dependsOn":[]},{"ref":"pkg:npm/doctrine@3.0.0","dependsOn":["pkg:npm/esutils@2.0.3"]},{"ref":"pkg:npm/escape-string-regexp@4.0.0","dependsOn":[]},{"ref":"pkg:npm/estraverse@5.3.0","dependsOn":[]},{"ref":"pkg:npm/esrecurse@4.3.0","dependsOn":["pkg:npm/estraverse@5.3.0"]},{"ref":"pkg:npm/eslint-scope@7.2.2","dependsOn":["pkg:npm/esrecurse@4.3.0","pkg:npm/estraverse@5.3.0"]},{"ref":"pkg:npm/esquery@1.7.0","dependsOn":["pkg:npm/estraverse@5.3.0"]},{"ref":"pkg:npm/flatted@3.4.2","dependsOn":[]},{"ref":"pkg:npm/json-buffer@3.0.1","dependsOn":[]},{"ref":"pkg:npm/keyv@4.5.4","dependsOn":["pkg:npm/json-buffer@3.0.1"]},{"ref":"pkg:npm/rimraf@3.0.2","dependsOn":["pkg:npm/glob@7.2.3"]},{"ref":"pkg:npm/flat-cache@3.2.0","dependsOn":["pkg:npm/flatted@3.4.2","pkg:npm/keyv@4.5.4","pkg:npm/rimraf@3.0.2"]},{"ref":"pkg:npm/file-entry-cache@6.0.1","dependsOn":["pkg:npm/flat-cache@3.2.0"]},{"ref":"pkg:npm/p-locate@5.0.0","dependsOn":["pkg:npm/p-limit@3.1.0"]},{"ref":"pkg:npm/locate-path@6.0.0","dependsOn":["pkg:npm/p-locate@5.0.0"]},{"ref":"pkg:npm/find-up@5.0.0","dependsOn":["pkg:npm/locate-path@6.0.0","pkg:npm/path-exists@4.0.0"]},{"ref":"pkg:npm/glob-parent@6.0.2","dependsOn":["pkg:npm/is-glob@4.0.3"]},{"ref":"pkg:npm/graphemer@1.4.0","dependsOn":[]},{"ref":"pkg:npm/is-path-inside@3.0.3","dependsOn":[]},{"ref":"pkg:npm/json-stable-stringify-without-jsonify@1.0.1","dependsOn":[]},{"ref":"pkg:npm/prelude-ls@1.2.1","dependsOn":[]},{"ref":"pkg:npm/type-check@0.4.0","dependsOn":["pkg:npm/prelude-ls@1.2.1"]},{"ref":"pkg:npm/levn@0.4.1","dependsOn":["pkg:npm/prelude-ls@1.2.1","pkg:npm/type-check@0.4.0"]},{"ref":"pkg:npm/lodash.merge@4.6.2","dependsOn":[]},{"ref":"pkg:npm/deep-is@0.1.4","dependsOn":[]},{"ref":"pkg:npm/fast-levenshtein@2.0.6","dependsOn":[]},{"ref":"pkg:npm/word-wrap@1.2.5","dependsOn":[]},{"ref":"pkg:npm/optionator@0.9.4","dependsOn":["pkg:npm/deep-is@0.1.4","pkg:npm/fast-levenshtein@2.0.6","pkg:npm/levn@0.4.1","pkg:npm/prelude-ls@1.2.1","pkg:npm/type-check@0.4.0","pkg:npm/word-wrap@1.2.5"]},{"ref":"pkg:npm/text-table@0.2.0","dependsOn":[]},{"ref":"pkg:npm/eslint@8.57.1","dependsOn":["pkg:npm/@eslint-community/eslint-utils@4.9.1","pkg:npm/@eslint-community/regexpp@4.12.2","pkg:npm/@eslint/eslintrc@2.1.4","pkg:npm/@eslint/js@8.57.1","pkg:npm/@humanwhocodes/config-array@0.13.0","pkg:npm/@humanwhocodes/module-importer@1.0.1","pkg:npm/@nodelib/fs.walk@1.2.8","pkg:npm/@ungap/structured-clone@1.3.2","pkg:npm/ajv@6.15.0","pkg:npm/chalk@4.1.2","pkg:npm/cross-spawn@7.0.6","pkg:npm/debug@4.4.3","pkg:npm/doctrine@3.0.0","pkg:npm/escape-string-regexp@4.0.0","pkg:npm/eslint-scope@7.2.2","pkg:npm/eslint-visitor-keys@3.4.3","pkg:npm/espree@9.6.1","pkg:npm/esquery@1.7.0","pkg:npm/esutils@2.0.3","pkg:npm/fast-deep-equal@3.1.3","pkg:npm/file-entry-cache@6.0.1","pkg:npm/find-up@5.0.0","pkg:npm/glob-parent@6.0.2","pkg:npm/globals@13.24.0","pkg:npm/graphemer@1.4.0","pkg:npm/ignore@5.3.2","pkg:npm/imurmurhash@0.1.4","pkg:npm/is-glob@4.0.3","pkg:npm/is-path-inside@3.0.3","pkg:npm/js-yaml@4.3.0","pkg:npm/json-stable-stringify-without-jsonify@1.0.1","pkg:npm/levn@0.4.1","pkg:npm/lodash.merge@4.6.2","pkg:npm/minimatch@3.1.5","pkg:npm/natural-compare@1.4.0","pkg:npm/optionator@0.9.4","pkg:npm/strip-ansi@6.0.1","pkg:npm/text-table@0.2.0"]},{"ref":"pkg:npm/prettier@3.9.0","dependsOn":[]},{"ref":"pkg:npm/sbom-example-nodejs@1.0.0","dependsOn":["pkg:npm/axios@1.18.1","pkg:npm/compression@1.8.1","pkg:npm/cors@2.8.6","pkg:npm/dotenv@16.6.1","pkg:npm/eslint@8.57.1","pkg:npm/express@4.22.2","pkg:npm/helmet@7.2.0","pkg:npm/jest@29.7.0","pkg:npm/lodash@4.18.1","pkg:npm/moment@2.30.1","pkg:npm/morgan@1.11.0","pkg:npm/nodemon@3.1.14","pkg:npm/prettier@3.9.0","pkg:npm/supertest@6.3.4","pkg:npm/winston@3.19.0"]}]} \ No newline at end of file diff --git a/apps/backend/tests/unit/integrations/test_sbom_scope_filter.py b/apps/backend/tests/unit/integrations/test_sbom_scope_filter.py new file mode 100644 index 00000000..d9f9e0d1 --- /dev/null +++ b/apps/backend/tests/unit/integrations/test_sbom_scope_filter.py @@ -0,0 +1,330 @@ +"""Unit tests — runtime-scope SBOM post-filter (Phase K). + +The filter's contract has two halves, and both are safety-critical: + + * it must drop exactly the non-deployable set (maven ``optional``/ + ``excluded``, npm lockfile-``dev``) and prune the dependency graph to the + survivors, always preserving the ``metadata.component`` root ref; + * it must be structurally incapable of *over*-dropping: the hasScopes / + hasDev guards no-op the whole pass when the evidence to filter safely is + absent, an npm purl missing from the lockfile is kept (keep-if-unknown), + and any error leaves the document untouched with ``applied=False``. + +The module is pure (no DB / network / subprocess), mirroring +``integrations.npm_lockfile`` — the SBOM dict in, mutated dict out. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from integrations.npm_lockfile import NpmLockfileData +from integrations.sbom_scope_filter import ( + FILTER_PROPERTY_NAME, + filter_sbom_to_runtime_scope, + rewrite_sbom_file, +) + +# --------------------------------------------------------------------------- +# Builders +# --------------------------------------------------------------------------- + + +def _maven(name: str, scope: str | None = None) -> dict[str, Any]: + purl = f"pkg:maven/com.example/{name}@1.0.0" + comp: dict[str, Any] = { + "type": "library", + "name": name, + "version": "1.0.0", + "purl": purl, + "bom-ref": purl, + } + if scope is not None: + comp["scope"] = scope + return comp + + +def _npm(name: str, version: str = "1.0.0") -> dict[str, Any]: + purl = f"pkg:npm/{name}@{version}" + return { + "type": "library", + "name": name, + "version": version, + "purl": purl, + "bom-ref": purl, + } + + +def _sbom( + components: list[dict[str, Any]], + dependencies: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + return { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "metadata": { + "component": { + "type": "application", + "name": "app-root", + "bom-ref": "pkg:maven/com.example/app-root@1.0.0", + "purl": "pkg:maven/com.example/app-root@1.0.0", + } + }, + "components": components, + "dependencies": dependencies if dependencies is not None else [], + } + + +def _lock(scopes: dict[str, str]) -> NpmLockfileData: + return NpmLockfileData(scope_by_purl=scopes, adjacency={}) + + +# --------------------------------------------------------------------------- +# Maven predicate +# --------------------------------------------------------------------------- + + +def test_maven_drops_optional_and_excluded_keeps_required_and_unscoped() -> None: + sbom = _sbom( + [ + _maven("spring-core", "required"), + _maven("junit", "optional"), + _maven("lombok", "excluded"), + _maven("unscoped-lib"), # no scope tag — keep-if-unknown + ] + ) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.applied is True + assert result.dropped == {"maven": 2} + names = [c["name"] for c in sbom["components"]] + assert names == ["spring-core", "unscoped-lib"] + assert result.kept_components == 2 + + +def test_maven_has_scopes_guard_noop_when_no_required_scope() -> None: + # An SBOM whose producer populated no ``required`` scope (fallback + # generators) must be left untouched even if optional tags appear. + sbom = _sbom([_maven("a", "optional"), _maven("b"), _maven("c", "excluded")]) + before = json.loads(json.dumps(sbom)) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.applied is False + assert sbom == before + + +def test_maven_predicate_disabled_by_flag() -> None: + sbom = _sbom([_maven("keep", "required"), _maven("junit", "optional")]) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None, maven=False) + assert result.applied is False + assert len(sbom["components"]) == 2 + + +def test_non_maven_purl_untouched_by_maven_predicate() -> None: + pypi = { + "type": "library", + "name": "requests", + "version": "2.31.0", + "purl": "pkg:pypi/requests@2.31.0", + "scope": "optional", # pypi optional is NOT the maven contract — keep + } + sbom = _sbom([_maven("keep", "required"), _maven("junit", "optional"), pypi]) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.dropped == {"maven": 1} + assert any(c["name"] == "requests" for c in sbom["components"]) + + +# --------------------------------------------------------------------------- +# Node predicate +# --------------------------------------------------------------------------- + + +def test_node_drops_lockfile_dev_entries() -> None: + lock = _lock( + { + "pkg:npm/express@4.18.2": "required", + "pkg:npm/jest@29.7.0": "dev", + "pkg:npm/eslint@8.55.0": "dev", + } + ) + sbom = _sbom([_npm("express", "4.18.2"), _npm("jest", "29.7.0"), _npm("eslint", "8.55.0")]) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=lock) + assert result.applied is True + assert result.dropped == {"npm": 2} + assert [c["name"] for c in sbom["components"]] == ["express"] + + +def test_node_keep_if_unknown_purl_absent_from_lockfile() -> None: + # A nested monorepo manifest's packages are not in the root lockfile — + # the filter may only remove components with positive dev evidence. + lock = _lock({"pkg:npm/jest@29.7.0": "dev"}) + sbom = _sbom([_npm("jest", "29.7.0"), _npm("nested-only", "2.0.0")]) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=lock) + assert result.dropped == {"npm": 1} + assert [c["name"] for c in sbom["components"]] == ["nested-only"] + + +def test_node_has_dev_guard_noop_without_dev_entries() -> None: + lock = _lock({"pkg:npm/express@4.18.2": "required"}) + sbom = _sbom([_npm("express", "4.18.2")]) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=lock) + assert result.applied is False + + +def test_node_noop_when_lockfile_missing() -> None: + sbom = _sbom([_npm("express", "4.18.2")]) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.applied is False + + +def test_node_predicate_disabled_by_flag() -> None: + lock = _lock({"pkg:npm/jest@29.7.0": "dev"}) + sbom = _sbom([_npm("jest", "29.7.0")]) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=lock, node=False) + assert result.applied is False + assert len(sbom["components"]) == 1 + + +# --------------------------------------------------------------------------- +# Graph pruning + root preservation +# --------------------------------------------------------------------------- + + +def test_dependency_graph_pruned_to_kept_refs_and_root_preserved() -> None: + root_ref = "pkg:maven/com.example/app-root@1.0.0" + keep_ref = "pkg:maven/com.example/spring-core@1.0.0" + drop_ref = "pkg:maven/com.example/junit@1.0.0" + sbom = _sbom( + [_maven("spring-core", "required"), _maven("junit", "optional")], + dependencies=[ + {"ref": root_ref, "dependsOn": [keep_ref, drop_ref]}, + {"ref": keep_ref, "dependsOn": [drop_ref]}, + {"ref": drop_ref, "dependsOn": []}, + ], + ) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.dropped == {"maven": 1} + deps = {d["ref"]: d["dependsOn"] for d in sbom["dependencies"]} + # Dropped component's own entry removed; surviving dependsOn filtered. + assert drop_ref not in deps + assert deps[root_ref] == [keep_ref] + assert deps[keep_ref] == [] + + +def test_root_ref_entry_survives_even_if_root_not_in_components() -> None: + # metadata.component rarely appears in components[]; its graph entry must + # survive regardless (BomLens always adds the root to keptRefs). + root_ref = "pkg:maven/com.example/app-root@1.0.0" + sbom = _sbom( + [_maven("spring-core", "required"), _maven("junit", "optional")], + dependencies=[{"ref": root_ref, "dependsOn": []}], + ) + filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert any(d.get("ref") == root_ref for d in sbom["dependencies"]) + + +# --------------------------------------------------------------------------- +# Transparency property +# --------------------------------------------------------------------------- + + +def test_filter_property_stamped_with_per_ecosystem_counts() -> None: + lock = _lock({"pkg:npm/jest@29.7.0": "dev"}) + sbom = _sbom( + [_maven("keep", "required"), _maven("junit", "optional"), _npm("jest", "29.7.0")] + ) + filter_sbom_to_runtime_scope(sbom, npm_lock=lock) + props = sbom["metadata"]["properties"] + stamped = [p for p in props if p["name"] == FILTER_PROPERTY_NAME] + assert stamped == [{"name": FILTER_PROPERTY_NAME, "value": "maven=1,npm=1"}] + + +def test_filter_property_idempotent_on_rerun() -> None: + sbom = _sbom([_maven("keep", "required"), _maven("junit", "optional")]) + filter_sbom_to_runtime_scope(sbom, npm_lock=None) + # Second pass over an already-filtered document: nothing left to drop, so + # the original stamp must survive exactly once (no duplicates). + filter_sbom_to_runtime_scope(sbom, npm_lock=None) + stamps = [ + p + for p in sbom["metadata"]["properties"] + if p["name"] == FILTER_PROPERTY_NAME + ] + assert len(stamps) == 1 + + +def test_no_property_and_no_mutation_when_nothing_dropped() -> None: + sbom = _sbom( + [_maven("a", "required"), _maven("b", "required")], + dependencies=[{"ref": "x", "dependsOn": ["y"]}], + ) + before = json.loads(json.dumps(sbom)) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.applied is True # predicate ran — it just found nothing + assert result.dropped == {} + assert sbom == before # zero mutation: graph + properties untouched + + +# --------------------------------------------------------------------------- +# Adversarial / degradation +# --------------------------------------------------------------------------- + + +def test_components_not_a_list_degrades_to_noop() -> None: + sbom = {"components": {"not": "a list"}} + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.applied is False + + +def test_non_dict_component_entries_are_kept() -> None: + sbom = _sbom([_maven("keep", "required"), _maven("junit", "optional")]) + sbom["components"].append("not-a-dict") + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.dropped == {"maven": 1} + assert "not-a-dict" in sbom["components"] + + +def test_missing_dependencies_array_tolerated() -> None: + sbom = _sbom([_maven("keep", "required"), _maven("junit", "optional")]) + del sbom["dependencies"] + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.dropped == {"maven": 1} + assert "dependencies" not in sbom + + +def test_non_string_purl_kept() -> None: + sbom = _sbom([_maven("keep", "required")]) + sbom["components"].append({"name": "weird", "purl": 42, "scope": "optional"}) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert any(c.get("name") == "weird" for c in sbom["components"] if isinstance(c, dict)) + assert result.dropped == {} + + +# --------------------------------------------------------------------------- +# rewrite_sbom_file +# --------------------------------------------------------------------------- + + +def test_rewrite_sbom_file_atomic_success(tmp_path: Path) -> None: + target = tmp_path / "bom.json" + target.write_text('{"old": true}', encoding="utf-8") + assert rewrite_sbom_file(target, {"new": True}) is True + assert json.loads(target.read_text(encoding="utf-8")) == {"new": True} + # No temp litter left behind. + assert [p.name for p in tmp_path.iterdir()] == ["bom.json"] + + +def test_rewrite_sbom_file_failure_leaves_original_and_returns_false( + tmp_path: Path, +) -> None: + target = tmp_path / "missing-dir" / "bom.json" # parent does not exist + assert rewrite_sbom_file(target, {"new": True}) is False + + +def test_rewrite_sbom_file_unserializable_payload_keeps_original( + tmp_path: Path, +) -> None: + target = tmp_path / "bom.json" + target.write_text('{"old": true}', encoding="utf-8") + assert rewrite_sbom_file(target, {"bad": object()}) is False + assert json.loads(target.read_text(encoding="utf-8")) == {"old": True} diff --git a/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py new file mode 100644 index 00000000..a4893605 --- /dev/null +++ b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py @@ -0,0 +1,350 @@ +"""Unit tests — runtime-scope filter pipeline integration (Phase K). + +Three layers are pinned here, each against REAL captured tool output (the +persist-boundary rule — hand-rolled minimal JSON hides the defect habitat): + + * **Fixture-scale filter behaviour** — ``real_cyclonedx_maven_scoped.json`` + (cdxgen 12.3.3 over a Spring Boot 3.2 Maven project: 108 components, + required/optional/excluded/unscoped all present) and + ``real_cyclonedx_node_dev.json`` + ``npm/package-lock.devdeps.json`` + (cdxgen + npm lockfile for an Express app with 5 devDependency roots). + * **Persist boundary** — the filtered document flows through + ``persist_sbom_components`` and the row set matches the kept set. + * **Wiring** — ``_apply_scope_filter``'s copy-then-commit contract (disk and + memory can never diverge) and the pipeline ordering invariant (filter + BEFORE artifact persist and signing — a refactor that moves signing + earlier would silently sign the unfiltered document). +""" + +from __future__ import annotations + +import ast +import copy +import json +import shutil +import uuid +from pathlib import Path +from typing import Any + +import pytest + +from integrations.cdxgen import CdxgenResult +from integrations.npm_lockfile import read_lockfile +from integrations.sbom_scope_filter import filter_sbom_to_runtime_scope + +FIXTURES = Path(__file__).resolve().parents[2] / "fixtures" +MAVEN_SBOM = FIXTURES / "sbom" / "real_cyclonedx_maven_scoped.json" +NODE_SBOM = FIXTURES / "sbom" / "real_cyclonedx_node_dev.json" +NODE_LOCK = FIXTURES / "npm" / "package-lock.devdeps.json" + + +def _load(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _refs(components: list[dict[str, Any]]) -> set[str]: + return { + c.get("bom-ref") or c.get("purl") + for c in components + if isinstance(c, dict) and (c.get("bom-ref") or c.get("purl")) + } + + +# --------------------------------------------------------------------------- +# Real maven fixture +# --------------------------------------------------------------------------- + + +def test_maven_fixture_drops_exactly_the_non_deployable_set() -> None: + sbom = _load(MAVEN_SBOM) + components = sbom["components"] + expected_dropped = sum( + 1 + for c in components + if isinstance(c.get("purl"), str) + and c["purl"].startswith("pkg:maven/") + and c.get("scope") in {"optional", "excluded"} + ) + assert expected_dropped > 0 # fixture must exercise the drop path + total_before = len(components) + + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + + assert result.applied is True + assert result.dropped == {"maven": expected_dropped} + assert len(sbom["components"]) == total_before - expected_dropped + # Survivors carry no droppable scope. + assert not any( + c.get("scope") in {"optional", "excluded"} + for c in sbom["components"] + if isinstance(c.get("purl"), str) and c["purl"].startswith("pkg:maven/") + ) + + +def test_maven_fixture_graph_has_no_dangling_refs_after_filter() -> None: + sbom = _load(MAVEN_SBOM) + filter_sbom_to_runtime_scope(sbom, npm_lock=None) + + kept = _refs(sbom["components"]) + metadata_component = sbom.get("metadata", {}).get("component", {}) + root_ref = metadata_component.get("bom-ref") or metadata_component.get("purl") + if root_ref: + kept.add(root_ref) + + for entry in sbom.get("dependencies", []): + assert entry["ref"] in kept, f"dangling graph entry: {entry['ref']}" + for child in entry.get("dependsOn", []): + assert child in kept, f"dangling dependsOn: {child}" + + +# --------------------------------------------------------------------------- +# Real node fixture + real lockfile +# --------------------------------------------------------------------------- + + +@pytest.fixture +def node_source_dir(tmp_path: Path) -> Path: + shutil.copy(NODE_LOCK, tmp_path / "package-lock.json") + return tmp_path + + +def test_node_fixture_drops_lockfile_dev_set(node_source_dir: Path) -> None: + sbom = _load(NODE_SBOM) + npm_lock = read_lockfile(node_source_dir) + assert npm_lock is not None + + expected_dropped = sum( + 1 + for c in sbom["components"] + if isinstance(c.get("purl"), str) + and c["purl"].startswith("pkg:npm/") + and npm_lock.scope_for_purl(c["purl"]) == "dev" + ) + assert expected_dropped > 0 + total_before = len(sbom["components"]) + + result = filter_sbom_to_runtime_scope(sbom, npm_lock=npm_lock) + + assert result.applied is True + assert result.dropped == {"npm": expected_dropped} + assert len(sbom["components"]) == total_before - expected_dropped + + names = {c.get("name") for c in sbom["components"]} + # devDependency roots gone, production roots kept. + assert "jest" not in names + assert "eslint" not in names + assert "nodemon" not in names + assert "express" in names + assert "lodash" in names + + +# --------------------------------------------------------------------------- +# Persist boundary — filtered document through persist_sbom_components +# --------------------------------------------------------------------------- + + +class _FakeComponent: + def __init__(self) -> None: + self.id = uuid.uuid4() + + +class _FakeComponentVersion: + def __init__(self) -> None: + self.id = uuid.uuid4() + + +class _FakeSession: + def __init__(self) -> None: + self.added: list[Any] = [] + + def add(self, row: Any) -> None: + self.added.append(row) + + +@pytest.fixture +def patched_helpers(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + "tasks.scan_source._get_or_create_component", + lambda session, *, purl, name, package_type: _FakeComponent(), + ) + monkeypatch.setattr( + "tasks.scan_source._get_or_create_component_version", + lambda session, *, component, version, purl_with_version: _FakeComponentVersion(), + ) + monkeypatch.setattr( + "tasks.scan_source._persist_component_licenses", + lambda session, *, scan_uuid, component_version_id, cdxgen_component, purl: None, + ) + + def _no_graph(session: Any, **kwargs: Any) -> None: + return None + + monkeypatch.setattr("tasks.scan_source._persist_dependency_graph", _no_graph) + + +def test_persist_after_filter_persists_exactly_the_kept_set( + patched_helpers: None, +) -> None: + from models import ScanComponent + from tasks.scan_source import persist_sbom_components + + sbom = _load(MAVEN_SBOM) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=None) + assert result.dropped # sanity: filter did something + + session = _FakeSession() + persist_sbom_components(session, scan_uuid=uuid.uuid4(), sbom=sbom) + + rows = [r for r in session.added if isinstance(r, ScanComponent)] + persistable = [ + c for c in sbom["components"] if isinstance(c.get("purl"), str) and c["purl"] + ] + assert len(rows) == len(persistable) + # No persisted row may carry a droppable maven scope — the filter ran first. + assert not any(r.dependency_scope in {"optional", "excluded"} for r in rows) + + +def test_persist_alone_does_not_filter(patched_helpers: None) -> None: + """persist_sbom_components is filter-free by design — the ingest path + (uploaded SBOMs, the supplier's declared truth) relies on this.""" + from models import ScanComponent + from tasks.scan_source import persist_sbom_components + + sbom = _load(MAVEN_SBOM) # UNfiltered — optional/excluded still present + total = sum( + 1 for c in sbom["components"] if isinstance(c.get("purl"), str) and c["purl"] + ) + session = _FakeSession() + persist_sbom_components(session, scan_uuid=uuid.uuid4(), sbom=sbom) + rows = [r for r in session.added if isinstance(r, ScanComponent)] + assert len(rows) == total + assert any(r.dependency_scope == "optional" for r in rows) + + +# --------------------------------------------------------------------------- +# _apply_scope_filter wiring — copy-then-commit contract +# --------------------------------------------------------------------------- + + +def _make_cdxgen_result(tmp_path: Path, sbom: dict[str, Any]) -> CdxgenResult: + sbom_path = tmp_path / "bom.json" + sbom_path.write_text(json.dumps(sbom), encoding="utf-8") + return CdxgenResult(sbom_path=sbom_path, sbom=copy.deepcopy(sbom)) + + +def test_apply_scope_filter_rewrites_disk_and_memory_consistently( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from tasks import scan_source + + recorded: list[Any] = [] + monkeypatch.setattr( + scan_source, "_record_scope_filter", lambda scan_uuid, result: recorded.append(result) + ) + original = _load(MAVEN_SBOM) + cdxgen_result = _make_cdxgen_result(tmp_path, original) + + scan_source._apply_scope_filter( + scan_uuid=uuid.uuid4(), cdxgen_result=cdxgen_result, source_dir=tmp_path + ) + + on_disk = _load(cdxgen_result.sbom_path) + assert on_disk == cdxgen_result.sbom # memory and disk identical + assert len(on_disk["components"]) < len(original["components"]) + assert recorded and recorded[0].dropped + + +def test_apply_scope_filter_master_switch_off_is_a_full_noop( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from tasks import scan_source + + monkeypatch.setattr(scan_source, "scan_scope_filter_enabled", lambda: False) + original = _load(MAVEN_SBOM) + cdxgen_result = _make_cdxgen_result(tmp_path, original) + + scan_source._apply_scope_filter( + scan_uuid=uuid.uuid4(), cdxgen_result=cdxgen_result, source_dir=tmp_path + ) + + assert _load(cdxgen_result.sbom_path) == original + assert cdxgen_result.sbom == original + + +def test_apply_scope_filter_rewrite_failure_keeps_memory_and_disk_unfiltered( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The divergence guard: if the disk rewrite fails, the in-memory document + must NOT be swapped — persist and Trivy keep seeing the same (unfiltered) + SBOM, a degraded-but-consistent outcome.""" + from tasks import scan_source + + monkeypatch.setattr( + scan_source.sbom_scope_filter, "rewrite_sbom_file", lambda path, sbom: False + ) + recorded: list[Any] = [] + monkeypatch.setattr( + scan_source, "_record_scope_filter", lambda scan_uuid, result: recorded.append(result) + ) + original = _load(MAVEN_SBOM) + cdxgen_result = _make_cdxgen_result(tmp_path, original) + + scan_source._apply_scope_filter( + scan_uuid=uuid.uuid4(), cdxgen_result=cdxgen_result, source_dir=tmp_path + ) + + assert cdxgen_result.sbom == original # memory untouched + assert _load(cdxgen_result.sbom_path) == original # disk untouched + assert recorded == [] # no telemetry claiming a filter that didn't land + + +# --------------------------------------------------------------------------- +# Lifecycle-sequence pins (CLAUDE.md hardening rule 5) +# --------------------------------------------------------------------------- + + +def _call_order(func_name: str, module_path: Path) -> list[str]: + """First-call order of named functions inside ``func_name`` (AST walk).""" + tree = ast.parse(module_path.read_text(encoding="utf-8")) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name == func_name: + order: list[str] = [] + for call in ast.walk(node): + if isinstance(call, ast.Call): + callee = call.func + name = ( + callee.id + if isinstance(callee, ast.Name) + else callee.attr + if isinstance(callee, ast.Attribute) + else None + ) + if name and name not in order: + order.append(name) + return order + raise AssertionError(f"{func_name} not found in {module_path}") + + +def test_pipeline_filters_before_artifact_persist_and_signing() -> None: + """A refactor that moves ``_persist_artifact`` or ``_sign_sbom`` ahead of + the scope filter would persist/sign the UNfiltered document while Trivy + and the component rows see the filtered one. Pin the order.""" + import tasks.scan_source as scan_source_module + + order = _call_order("_run_pipeline", Path(scan_source_module.__file__)) + assert order.index("_apply_scope_filter") < order.index("_persist_artifact") + assert order.index("_apply_scope_filter") < order.index("_sign_sbom") + + +def test_ingest_pipeline_never_references_the_scope_filter() -> None: + """The ingest path must NOT filter (uploaded SBOMs are declared truth) — + see the deliberate-divergence comment in tasks/ingest_sbom.py.""" + import tasks.ingest_sbom as ingest_module + + source = Path(ingest_module.__file__).read_text(encoding="utf-8") + tree = ast.parse(source) + names = { + node.id for node in ast.walk(tree) if isinstance(node, ast.Name) + } | {node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute)} + assert "_apply_scope_filter" not in names + assert "filter_sbom_to_runtime_scope" not in names diff --git a/apps/backend/tests/unit/test_scope_filter_config.py b/apps/backend/tests/unit/test_scope_filter_config.py new file mode 100644 index 00000000..bf235559 --- /dev/null +++ b/apps/backend/tests/unit/test_scope_filter_config.py @@ -0,0 +1,66 @@ +""" +Unit tests for the runtime-scope filter config accessors (no DB) — Phase K. + +All three accessors read ``os.getenv`` at call time (CLAUDE.md core rule #11) +and default ON. The parsing is deliberately the INVERSE of ``scanoss_enabled``: +only the exact falsy tokens ``false`` / ``0`` / ``no`` disable, so a typo +fails OPEN to the correct-by-default filtering behaviour. SCANOSS fails closed +because its failure direction is "unexpected egress"; here the failure +direction is "silently reverting to over-counted CVEs", so open-to-correct is +the safe side. +""" + +from __future__ import annotations + +from collections.abc import Iterator + +import pytest + +from core.config import ( + scan_scope_filter_enabled, + scan_scope_filter_maven_enabled, + scan_scope_filter_node_enabled, +) + +_ENV_KEYS = ( + "SCAN_SCOPE_FILTER_ENABLED", + "SCAN_SCOPE_FILTER_MAVEN_ENABLED", + "SCAN_SCOPE_FILTER_NODE_ENABLED", +) + +_ACCESSOR_BY_KEY = { + "SCAN_SCOPE_FILTER_ENABLED": scan_scope_filter_enabled, + "SCAN_SCOPE_FILTER_MAVEN_ENABLED": scan_scope_filter_maven_enabled, + "SCAN_SCOPE_FILTER_NODE_ENABLED": scan_scope_filter_node_enabled, +} + + +@pytest.fixture(autouse=True) +def _clean_env(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: + for key in _ENV_KEYS: + monkeypatch.delenv(key, raising=False) + yield + + +@pytest.mark.parametrize("key", _ENV_KEYS) +def test_defaults_on(key: str) -> None: + assert _ACCESSOR_BY_KEY[key]() is True + + +@pytest.mark.parametrize("key", _ENV_KEYS) +@pytest.mark.parametrize("value", ["false", "FALSE", "False", "0", "no", " no "]) +def test_exact_falsy_tokens_disable( + key: str, value: str, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv(key, value) + assert _ACCESSOR_BY_KEY[key]() is False + + +@pytest.mark.parametrize("key", _ENV_KEYS) +@pytest.mark.parametrize("value", ["true", "1", "yes", "", " ", "off", "disable", "n"]) +def test_anything_else_stays_on( + key: str, value: str, monkeypatch: pytest.MonkeyPatch +) -> None: + # Fail-open-to-correct: typos ("off", "disable", "n") keep the filter ON. + monkeypatch.setenv(key, value) + assert _ACCESSOR_BY_KEY[key]() is True diff --git a/docs-site/docs/reference/env-variables.md b/docs-site/docs/reference/env-variables.md index 201e7edf..dbe42bb4 100644 --- a/docs-site/docs/reference/env-variables.md +++ b/docs-site/docs/reference/env-variables.md @@ -119,6 +119,9 @@ See [build gate](./glossary.md#build-gates) for the gate model and [Gate the bui | `SCANOSS_API_URL` | `https://api.osskb.org` | `config.py` | SCANOSS knowledge-base endpoint the fingerprints are matched against (used only when `SCANOSS_ENABLED=true`). Point this at a self-hosted SCANOSS instance to keep fingerprints inside your network. | | `SCANOSS_API_KEY` | *(empty)* | `config.py` | Optional API key for `SCANOSS_API_URL` (a paid / self-hosted endpoint). Empty uses the free `api.osskb.org` tier. | | `SCANOSS_TIMEOUT_SECONDS` | `300` | `config.py` | Hard wall-clock limit for the SCANOSS stage. On timeout the scan continues without vendored-OSS results (best-effort). | +| `SCAN_SCOPE_FILTER_ENABLED` | `true` | `config.py` | Master switch for the runtime-scope post-filter: source scans drop non-deployable dependencies (Maven `test`/`provided`, npm `devDependencies`) from the SBOM before persist, signing and Trivy matching. Pure local transformation, no egress. Only the exact tokens `false` / `0` / `no` disable it. See [Components & licenses → Runtime-scope filtering](../user-guide/components-and-licenses.md#runtime-scope-filtering). | +| `SCAN_SCOPE_FILTER_MAVEN_ENABLED` | `true` | `config.py` | Maven half of the scope filter (drops cdxgen scope `optional`/`excluded` nodes). Disable if a project relies on Maven `true` **runtime** dependencies — cdxgen tags those `optional` exactly like test scope, so they are dropped too. | +| `SCAN_SCOPE_FILTER_NODE_ENABLED` | `true` | `config.py` | npm half of the scope filter (drops packages the committed/generated `package-lock.json` classifies as `dev`). A package absent from the lockfile is always kept. | | `WORKSPACE_HOST_PATH` | `/tmp/trustedoss` | `config.py`, `docker-compose.yml` | Host directory mounted into the worker as `/workspace`. Holds repo clones + scan artefacts (cdxgen SBOM, scancode output). The compose stack overrides this to `/workspace` inside the container. | | `ORT_RULES_PATH` | `/opt/trustedoss/ort/rules.kts` | `docker-compose.yml` | Legacy path inside the worker, vestigial after the ORT stage was removed. The file is a placeholder and has no effect in this release — license-tier classification comes from `_LICENSE_CATEGORY_DEFAULTS` in `apps/backend/tasks/scan_source.py`. | | `JSONB_ROW_SIZE_LIMIT_BYTES` | `262144` (256 KB) | `config.py` | Per-row JSON byte ceiling before the writer truncates and emits a warning. Guards the I-1 unbounded-payload class. | diff --git a/docs-site/docs/user-guide/components-and-licenses.md b/docs-site/docs/user-guide/components-and-licenses.md index 25e9905d..1668b4aa 100644 --- a/docs-site/docs/user-guide/components-and-licenses.md +++ b/docs-site/docs/user-guide/components-and-licenses.md @@ -43,6 +43,38 @@ The inline filter bar at the top supports: Filters compose. The URL updates (`?direct=…`, `?dependency_scope=…`, …) so you can share a filtered view. +### Runtime-scope filtering {#runtime-scope-filtering} + +The table shows the **deployable runtime set**, not everything the build +resolved. cdxgen records every resolved node — including the test/dev +toolchain (`junit`, `lombok`, `jest`, `eslint`, …) that never ships with the +artifact — so by default the scanner drops those before the SBOM is persisted, +signed and matched against the vulnerability DB: + +- **Maven** — components cdxgen tagged `optional` (Maven `test` scope) or + `excluded` (`provided`/`system` scope) are removed. The filter only runs + when the SBOM actually carries scope tags; an SBOM without them is kept + whole, so recall never regresses. +- **npm** — packages the project's `package-lock.json` classifies as + `dev` are removed. A package the lockfile does not cover is always kept + (nested manifests in a monorepo are not covered by the root lockfile), so + the filter only removes components with positive dev-dependency evidence. + +The number of excluded components is recorded on the scan, and the SBOM's +`metadata.properties` carries a `trusca:scope_filter` entry with per-ecosystem +counts, so a filtered document self-documents what was removed. SBOMs uploaded +through the ingest API are **never** filtered — an uploaded SBOM is the +supplier's declared truth. + +Two caveats worth knowing: + +- cdxgen tags Maven `true` dependencies the same way as + `test` scope, so a rare *runtime* optional dependency is dropped with them. + Set `SCAN_SCOPE_FILTER_MAVEN_ENABLED=false` for such projects. +- Turning the filter off (`SCAN_SCOPE_FILTER_ENABLED=false`) restores the full + resolved graph on the next scan. See + [Environment variables](../reference/env-variables.md) for all three toggles. + ## Table view vs. graph view The Components tab has a **Table / Graph** toggle (top-left). **Table** is the default virtualized list above. **Graph** renders the scan's resolved **dependency graph** — every *parent → child* edge the scanner recorded — as an interactive node-link diagram (left-to-right layout), so you can see *how* a package is pulled in rather than just *that* it is present. Each node is coloured by its highest-severity finding (colour is never the only signal — the detail panel and the tree fallback show a severity label too), and a search box highlights matching packages. Clicking a node opens its details beside the canvas. diff --git a/docs-site/docs/user-guide/scans.md b/docs-site/docs/user-guide/scans.md index 70030c8d..11d02e08 100644 --- a/docs-site/docs/user-guide/scans.md +++ b/docs-site/docs/user-guide/scans.md @@ -144,7 +144,7 @@ The progress view shows real-time stage transitions: 1. **Bootstrapping** — preparing the workspace. 2. **Fetching source** — `git clone` (or `git fetch` + checkout for an existing workspace). -3. **Detecting components** — `cdxgen` walks the repo and emits a CycloneDX SBOM, with **declared** licenses read from each dependency's package metadata. +3. **Detecting components** — `cdxgen` walks the repo and emits a CycloneDX SBOM, with **declared** licenses read from each dependency's package metadata. Immediately after, the **runtime-scope post-filter** drops non-deployable dependencies (Maven `test`/`provided`, npm `devDependencies`) so every later stage — signing, persistence, vulnerability matching — sees the deployable set; see [Components & licenses → Runtime-scope filtering](./components-and-licenses.md#runtime-scope-filtering). 4. **Detecting first-party licenses** — scancode scans the project's own source files and records the **detected** licenses it finds, each tagged with the `source_path` of the file it came from (see [Components & licenses → Detected vs. declared](./components-and-licenses.md#declared-vs-detected)). This stage is best-effort: if scancode is not installed, times out, or the tree is too large, the scan continues with declared licenses only — a degraded but non-fatal outcome. Legal-tier classification is then applied from the built-in classifier catalog (see [Components & licenses → Classification source](./components-and-licenses.md#license-classification)). 5. **Resolving vulnerabilities** — `trivy sbom` matches the CycloneDX SBOM against the local Trivy DB (NVD + OSV + GHSA + EPSS + KEV). No network call per scan. 6. **Persisting** — components, licenses, and findings are written to PostgreSQL. @@ -253,13 +253,13 @@ If you build a custom client, the message shape is: ```json { - "step": "dt_findings", - "percent": 62, + "step": "trivy", + "percent": 90, "ts": "2026-05-09T13:42:11Z" } ``` -`percent` is an integer 0–100. `step` is one of the pipeline slugs (`bootstrap`, `fetch`, `prep`, `cdxgen`, `scancode`, `dt_upload`, `dt_findings`, `finalize`) plus the two terminal states (`succeeded`, `failed`). The `scancode` slug replaced the former `ort` slug at the same progress percent. The frame does not echo `scan_id` — the subscriber already knows it from the URL. +`percent` is an integer 0–100. `step` is one of the pipeline slugs (`bootstrap`, `fetch`, `prep`, `cdxgen`, `sign`, `scancode`, `approvals`, `scanoss`, `trivy`, `finalize`) plus the two terminal states (`succeeded`, `failed`). The `scancode` slug replaced the former `ort` slug, and `trivy` replaced the former `dt_upload`/`dt_findings` pair after the Dependency-Track removal (ADR-0001). The frame does not echo `scan_id` — the subscriber already knows it from the URL. ## Verify it worked diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md index 379b912f..60b7e1b5 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md @@ -118,6 +118,9 @@ CI 빌드 게이트는 기본적으로 Critical CVE와 금지 라이선스에서 | `SCANOSS_API_URL` | `https://api.osskb.org` | `config.py` | 핑거프린트를 매칭할 SCANOSS 지식 베이스 엔드포인트(`SCANOSS_ENABLED=true`일 때만 사용). 자체 호스팅 SCANOSS 인스턴스로 향하게 하면 핑거프린트가 사내에 머뭅니다. | | `SCANOSS_API_KEY` | *(빈 값)* | `config.py` | `SCANOSS_API_URL`용 선택 API 키(유료/자체 호스팅 엔드포인트). 비우면 무료 `api.osskb.org` 등급 사용. | | `SCANOSS_TIMEOUT_SECONDS` | `300` | `config.py` | SCANOSS 단계의 하드 wall-clock 제한. 타임아웃 시 vendored-OSS 결과 없이 스캔 계속(best-effort). | +| `SCAN_SCOPE_FILTER_ENABLED` | `true` | `config.py` | 런타임 스코프 필터의 마스터 스위치: 소스 스캔이 저장·서명·Trivy 매칭 전에 배포되지 않는 의존성(Maven `test`/`provided`, npm `devDependencies`)을 SBOM 에서 제거합니다. 외부 전송 없는 순수 로컬 변환입니다. 정확히 `false` / `0` / `no` 토큰만 끕니다. [컴포넌트·라이선스 → 런타임 스코프 필터링](../user-guide/components-and-licenses.md#runtime-scope-filtering) 참고. | +| `SCAN_SCOPE_FILTER_MAVEN_ENABLED` | `true` | `config.py` | 스코프 필터의 Maven 부분(cdxgen scope `optional`/`excluded` 노드 제거). 프로젝트가 Maven `true` **런타임** 의존성을 쓰면 끄십시오 — cdxgen 이 test scope 와 똑같이 `optional` 로 태깅해 함께 제거됩니다. | +| `SCAN_SCOPE_FILTER_NODE_ENABLED` | `true` | `config.py` | 스코프 필터의 npm 부분(커밋되었거나 prep 단계가 생성한 `package-lock.json` 이 `dev` 로 분류한 패키지 제거). lockfile 에 없는 패키지는 항상 유지합니다. | | `WORKSPACE_HOST_PATH` | `/tmp/trustedoss` | `config.py`, `docker-compose.yml` | worker에 `/workspace`로 마운트되는 호스트 디렉터리. 레포 클론 + 스캔 아티팩트(cdxgen SBOM, scancode 출력) 보관. compose 스택은 컨테이너 내에서 `/workspace`로 오버라이드합니다. | | `ORT_RULES_PATH` | `/opt/trustedoss/ort/rules.kts` | `docker-compose.yml` | worker 내부 레거시 경로로, ORT 단계 제거 후 잔재입니다. 파일은 placeholder 이며 v0.10.0 에서는 효과가 없습니다 — 라이선스 단계 분류는 `apps/backend/tasks/scan_source.py` 의 `_LICENSE_CATEGORY_DEFAULTS` 에서 옵니다. | | `JSONB_ROW_SIZE_LIMIT_BYTES` | `262144` (256 KB) | `config.py` | writer가 truncate + warn하기 전 행당 JSON 바이트 상한. I-1 무한 페이로드 클래스 가드. | diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md index bec7dfe4..d6ab79a3 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md @@ -43,6 +43,37 @@ sidebar_position: 3 필터는 결합됩니다. URL(`?direct=…`, `?dependency_scope=…`, …)이 갱신되어 필터된 뷰를 공유할 수 있습니다. +### 런타임 스코프 필터링 {#runtime-scope-filtering} + +테이블에는 빌드가 해석한 전부가 아니라 **배포되는 런타임 세트**가 표시됩니다. +cdxgen 은 해석된 모든 노드를 기록하므로, 산출물과 함께 배포되지 않는 테스트·개발 +도구(`junit`, `lombok`, `jest`, `eslint`, …)까지 SBOM 에 담깁니다. 기본 설정에서 +스캐너는 SBOM 을 저장·서명하고 취약점 DB 에 매칭하기 전에 이런 컴포넌트를 +제거합니다. + +- **Maven** — cdxgen 이 `optional`(Maven `test` scope) 또는 `excluded` + (`provided`/`system` scope)로 태깅한 컴포넌트를 제거합니다. 이 필터는 SBOM 에 + scope 태그가 실제로 존재할 때만 동작하며, 태그가 없는 SBOM 은 그대로 두므로 + 탐지 범위가 줄어드는 일은 없습니다. +- **npm** — 프로젝트의 `package-lock.json` 이 `dev` 로 분류한 패키지를 + 제거합니다. lockfile 이 다루지 않는 패키지는 항상 유지합니다(모노레포의 중첩 + manifest 는 루트 lockfile 에 없습니다). 즉 dev 의존성이라는 명확한 근거가 있는 + 컴포넌트만 제거합니다. + +제외된 컴포넌트 수는 스캔에 기록되고, SBOM 의 `metadata.properties` 에는 +생태계별 개수를 담은 `trusca:scope_filter` 항목이 남아 필터된 문서가 제거 내역을 +스스로 설명합니다. 인제스트 API 로 업로드한 SBOM 은 **절대** 필터링하지 +않습니다 — 업로드된 SBOM 은 공급자가 선언한 내용 그대로를 신뢰합니다. + +알아 둘 주의점 2가지: + +- cdxgen 은 Maven `true` 의존성을 `test` scope 와 같은 + 방식으로 태깅하므로, 드물게 *런타임* optional 의존성도 함께 제거됩니다. 그런 + 프로젝트에서는 `SCAN_SCOPE_FILTER_MAVEN_ENABLED=false` 로 끄십시오. +- 필터를 끄면(`SCAN_SCOPE_FILTER_ENABLED=false`) 다음 스캔부터 전체 해석 + 그래프가 복원됩니다. 토글 3종은 [환경변수](../reference/env-variables.md)를 + 참고하십시오. + ## 표 보기와 그래프 보기 Components 탭에는 **표 / 그래프** 토글이 있습니다(좌측 상단). 기본값은 위의 가상 스크롤 목록(**표**)입니다. **그래프**는 스캔이 해석한 **의존성 그래프**(스캐너가 기록한 모든 부모 → 자식 엣지)를 상호작용형 노드-링크 다이어그램(좌→우 배치)으로 렌더링합니다. 어떤 패키지가 단지 존재하는지가 아니라 어떻게 끌려 들어왔는지를 볼 수 있습니다. 각 노드는 가장 높은 심각도 결과에 따라 색으로 표시되며(색은 유일한 신호가 아닙니다 — 상세 패널과 트리 폴백에 심각도 라벨도 함께 표시), 검색 상자로 일치하는 패키지를 강조할 수 있습니다. 노드를 클릭하면 캔버스 옆에 상세가 열립니다. diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md index 0b9a2433..dae3754c 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md @@ -144,7 +144,7 @@ queued ─────► running ─────► succeeded 1. **Bootstrapping** — 작업 공간 준비. 2. **Fetching source** — `git clone`(또는 기존 작업 공간이면 `git fetch` + checkout). -3. **Detecting components** — `cdxgen`이 레포를 탐색하여 CycloneDX SBOM을 생성하고, 각 의존성의 패키지 메타데이터에서 **declared** 라이선스를 읽습니다. +3. **Detecting components** — `cdxgen`이 레포를 탐색하여 CycloneDX SBOM을 생성하고, 각 의존성의 패키지 메타데이터에서 **declared** 라이선스를 읽습니다. 직후 **런타임 스코프 필터**가 배포되지 않는 의존성(Maven `test`/`provided`, npm `devDependencies`)을 제거하여, 이후 모든 단계 — 서명·저장·취약점 매칭 — 가 배포 세트만 보게 합니다([컴포넌트·라이선스 → 런타임 스코프 필터링](./components-and-licenses.md#runtime-scope-filtering) 참고). 4. **Detecting first-party licenses** — scancode 가 프로젝트 자체 소스 파일을 스캔하여 발견한 **detected** 라이선스를 기록하며, 각 항목에 라이선스가 발견된 파일의 `source_path` 를 함께 태깅합니다([컴포넌트·라이선스 → declared vs. detected](./components-and-licenses.md#declared-vs-detected) 참고). 이 단계는 best-effort 입니다: scancode 가 미설치이거나 타임아웃이거나 트리가 너무 크면 declared 라이선스만으로 스캔을 계속합니다 — 저하되었으나 비치명적인 결과입니다. 이후 법적 단계 분류는 내장 분류 카탈로그에서 적용됩니다([컴포넌트·라이선스 → 분류 출처](./components-and-licenses.md#라이선스-분류) 참고). 5. **Resolving vulnerabilities** — `trivy sbom`이 CycloneDX SBOM을 로컬 Trivy DB(NVD + OSV + GHSA + EPSS + KEV)에 매칭. 스캔당 네트워크 호출 없음. 6. **Persisting** — 컴포넌트·라이선스·결과를 PostgreSQL에 저장. @@ -253,13 +253,13 @@ UI는 단계·진행률 실시간 갱신을 위해 `ws(s):///ws/scans/{sca ```json { - "step": "dt_findings", - "percent": 62, + "step": "trivy", + "percent": 90, "ts": "2026-05-09T13:42:11Z" } ``` -`percent`는 0–100 정수입니다. `step`은 파이프라인 슬러그(`bootstrap`, `fetch`, `prep`, `cdxgen`, `scancode`, `dt_upload`, `dt_findings`, `finalize`)와 2개의 종단 상태(`succeeded`, `failed`) 중 하나입니다. `scancode` 슬러그는 이전의 `ort` 슬러그를 같은 진행 percent 에서 대체했습니다. 프레임은 `scan_id`를 다시 보내지 않습니다 — 구독자가 URL에서 이미 알고 있기 때문입니다. +`percent`는 0–100 정수입니다. `step`은 파이프라인 슬러그(`bootstrap`, `fetch`, `prep`, `cdxgen`, `sign`, `scancode`, `approvals`, `scanoss`, `trivy`, `finalize`)와 2개의 종단 상태(`succeeded`, `failed`) 중 하나입니다. `scancode` 슬러그는 이전의 `ort` 슬러그를 대체했고, Dependency-Track 제거(ADR-0001) 이후 `trivy`가 이전의 `dt_upload`/`dt_findings` 쌍을 대체했습니다. 프레임은 `scan_id`를 다시 보내지 않습니다 — 구독자가 URL에서 이미 알고 있기 때문입니다. ## 정상 동작 확인 From 98dea274384bde1f08cbf26048752be592d79d17 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 00:28:03 +0900 Subject: [PATCH 02/10] feat(fe): surface the runtime-scope filter's excluded-components count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scope filter (previous commit) drops dev/test components server-side — without a visible trace, a re-scan that loses 300 components reads as data loss. Surface the worker telemetry in the Components tab: - useScanScopeFilter reads scan_metadata.scope_filter off the ordinary scan detail response (ScanPublic.metadata already carries the JSONB verbatim — no backend change). Scan resolution mirrors the tab's anchoring: pinned ?scan= wins, else the latest succeeded scan from the overview. The parser treats the blob as untrusted at every level and collapses all nothing-to- show shapes to null. - ComponentsTab summary band gains an additive 'excluded N' note (data-testid components-scope-filter-note, per-ecosystem breakdown on the title attribute); EN/KO keys in the same PR. - Seeds (seed_demo + seed_e2e_user) stamp deterministic scope_filter telemetry on their succeeded scans so the e2e spec asserts without a live worker run. --- apps/backend/scripts/seed_demo.py | 13 ++- apps/backend/scripts/seed_e2e_user.py | 13 ++- .../projects/api/useScanScopeFilter.ts | 74 +++++++++++++++ .../projects/components/ComponentsTab.tsx | 18 ++++ .../src/locales/en/project_detail.json | 1 + .../src/locales/ko/project_detail.json | 1 + .../tests/e2e/components_scope_filter.spec.ts | 91 +++++++++++++++++++ .../tests/unit/parseScopeFilter.test.ts | 54 +++++++++++ 8 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 apps/frontend/src/features/projects/api/useScanScopeFilter.ts create mode 100644 apps/frontend/tests/e2e/components_scope_filter.spec.ts create mode 100644 apps/frontend/tests/unit/parseScopeFilter.test.ts diff --git a/apps/backend/scripts/seed_demo.py b/apps/backend/scripts/seed_demo.py index e15bc690..244b7417 100644 --- a/apps/backend/scripts/seed_demo.py +++ b/apps/backend/scripts/seed_demo.py @@ -566,7 +566,18 @@ async def _seed() -> dict[str, Any]: # noqa: PLR0915 — single linear seed rea progress_percent=100, started_at=now - timedelta(minutes=12), completed_at=now - timedelta(minutes=4), - scan_metadata={"seeded_demo": True, "branch": "main"}, + scan_metadata={ + "seeded_demo": True, + "branch": "main", + # Phase K — deterministic runtime-scope filter telemetry + # so the Components summary band's "excluded N" note is + # e2e-testable without a live worker run. + "scope_filter": { + "applied": True, + "dropped": {"maven": 3, "npm": 12}, + "kept": 42, + }, + }, ) session.add(scan) scans.append(scan) diff --git a/apps/backend/scripts/seed_e2e_user.py b/apps/backend/scripts/seed_e2e_user.py index 6a3de8f9..dacc6c55 100644 --- a/apps/backend/scripts/seed_e2e_user.py +++ b/apps/backend/scripts/seed_e2e_user.py @@ -1166,7 +1166,18 @@ async def _seed( # noqa: PLR0915 — a single linear seed routine reads better progress_percent=100, started_at=datetime.now(tz=UTC), completed_at=datetime.now(tz=UTC), - scan_metadata={"seeded": True}, + scan_metadata={ + "seeded": True, + # Phase K — deterministic runtime-scope filter + # telemetry so components_scope_filter.spec.ts can + # assert the summary-band note without a live + # worker run. 3 + 12 = 15 is the asserted total. + "scope_filter": { + "applied": True, + "dropped": {"maven": 3, "npm": 12}, + "kept": 42, + }, + }, ) session.add(scan) await session.commit() diff --git a/apps/frontend/src/features/projects/api/useScanScopeFilter.ts b/apps/frontend/src/features/projects/api/useScanScopeFilter.ts new file mode 100644 index 00000000..ce5a2ffd --- /dev/null +++ b/apps/frontend/src/features/projects/api/useScanScopeFilter.ts @@ -0,0 +1,74 @@ +/** + * useScanScopeFilter — Phase K (PR K-2). + * + * Reads the runtime-scope filter telemetry the worker records on + * `Scan.scan_metadata.scope_filter` ({ applied, dropped, kept }) so the + * Components summary band can say "excluded N dev/test components". The + * telemetry rides the ordinary scan detail response (`ScanPublic.metadata`) + * — no dedicated endpoint. + * + * Scan resolution mirrors the tab's own data anchoring: a pinned `?scan=` + * wins; otherwise the latest *succeeded* scan from the overview's + * recent-scans list (the same scan the components list renders). Returns + * `null` while loading, when the scan has no telemetry (filter disabled, + * pre-Phase-K scan) or when nothing was dropped — callers render nothing in + * every null case. + */ +import { useQuery } from "@tanstack/react-query"; + +import type { ScanSummary } from "@/features/projects/api/projectDetailApi"; +import { getScan } from "@/lib/projectsApi"; + +export interface ScopeFilterTelemetry { + /** Per-ecosystem drop counts, e.g. { maven: 3, npm: 12 }. */ + dropped: Record; + /** Sum of all drops — the headline number. Always > 0 when non-null. */ + totalDropped: number; +} + +/** + * Defensive parse of `scan_metadata.scope_filter`. The blob is + * worker-written JSONB — treat every level as untyped and return `null` + * unless there is at least one positive drop count. + */ +export function parseScopeFilter( + metadata: Record | undefined, +): ScopeFilterTelemetry | null { + const raw = metadata?.["scope_filter"]; + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null; + const droppedRaw = (raw as Record)["dropped"]; + if ( + typeof droppedRaw !== "object" || + droppedRaw === null || + Array.isArray(droppedRaw) + ) { + return null; + } + const dropped: Record = {}; + let total = 0; + for (const [ecosystem, count] of Object.entries( + droppedRaw as Record, + )) { + if (typeof count === "number" && Number.isFinite(count) && count > 0) { + dropped[ecosystem] = count; + total += count; + } + } + return total > 0 ? { dropped, totalDropped: total } : null; +} + +export function useScanScopeFilter( + scanId: string | undefined, + recentScans: ScanSummary[] | undefined, +): ScopeFilterTelemetry | null { + const effectiveScanId = + scanId ?? recentScans?.find((scan) => scan.status === "succeeded")?.id; + const query = useQuery({ + queryKey: ["scans", effectiveScanId ?? "", "scope-filter"], + queryFn: () => getScan(effectiveScanId as string), + enabled: typeof effectiveScanId === "string" && effectiveScanId.length > 0, + // Telemetry is immutable once the scan is terminal — no refetch churn. + staleTime: 5 * 60 * 1000, + }); + return parseScopeFilter(query.data?.metadata); +} diff --git a/apps/frontend/src/features/projects/components/ComponentsTab.tsx b/apps/frontend/src/features/projects/components/ComponentsTab.tsx index e7f9d75e..770419ca 100644 --- a/apps/frontend/src/features/projects/components/ComponentsTab.tsx +++ b/apps/frontend/src/features/projects/components/ComponentsTab.tsx @@ -30,6 +30,7 @@ import type { SortOrder, } from "@/features/projects/api/projectDetailApi"; import { useComponents } from "@/features/projects/api/useComponents"; +import { useScanScopeFilter } from "@/features/projects/api/useScanScopeFilter"; import { ActiveFilterChips } from "@/features/projects/components/ActiveFilterChips"; import { ComponentDrawer } from "@/features/projects/components/ComponentDrawer"; import { DependencyGraph } from "@/features/projects/components/DependencyGraph"; @@ -381,6 +382,10 @@ export function ComponentsTab({ projectId, scanId }: ComponentsTabProps) { const severityDistribution = overview.data?.severity_distribution; const licenseDistribution = overview.data?.license_distribution; + // Phase K — runtime-scope filter transparency. Non-null only when the + // rendered scan's worker telemetry says components were actually dropped. + const scopeFilter = useScanScopeFilter(scanId, overview.data?.recent_scans); + return (
{/* View toggle — Table (virtual list) vs Graph (dependency graph, H-1). @@ -530,6 +535,19 @@ export function ComponentsTab({ projectId, scanId }: ComponentsTabProps) { {t("components.summary", { loaded: items.length, total })} + {scopeFilter ? ( + `${ecosystem}: ${count}`) + .join(", ")} + > + {t("components.summary_scope_filtered", { + count: scopeFilter.totalDropped, + })} + + ) : null}
{components.isError ? ( diff --git a/apps/frontend/src/locales/en/project_detail.json b/apps/frontend/src/locales/en/project_detail.json index 822d243b..527a2ce2 100644 --- a/apps/frontend/src/locales/en/project_detail.json +++ b/apps/frontend/src/locales/en/project_detail.json @@ -224,6 +224,7 @@ }, "components": { "summary": "{{loaded}} of {{total}} components loaded.", + "summary_scope_filtered": "{{count}} dev/test components excluded by the runtime-scope filter", "vulns_link_aria": "View {{count}} CVEs for {{name}} in the Vulnerabilities tab", "col": { "name": "Component", diff --git a/apps/frontend/src/locales/ko/project_detail.json b/apps/frontend/src/locales/ko/project_detail.json index 756f6bbc..842cb2b0 100644 --- a/apps/frontend/src/locales/ko/project_detail.json +++ b/apps/frontend/src/locales/ko/project_detail.json @@ -224,6 +224,7 @@ }, "components": { "summary": "전체 {{total}}개 중 {{loaded}}개 로드됨.", + "summary_scope_filtered": "런타임 스코프 필터가 dev/test 컴포넌트 {{count}}개를 제외함", "vulns_link_aria": "{{name}}의 CVE {{count}}건을 취약점 탭에서 보기", "col": { "name": "컴포넌트", diff --git a/apps/frontend/tests/e2e/components_scope_filter.spec.ts b/apps/frontend/tests/e2e/components_scope_filter.spec.ts new file mode 100644 index 00000000..5bbd7ea1 --- /dev/null +++ b/apps/frontend/tests/e2e/components_scope_filter.spec.ts @@ -0,0 +1,91 @@ +/** + * Components scope-filter note E2E — Phase K (PR K-2). + * + * The runtime-scope filter (backend PR K-1) drops dev/test dependencies from + * source-scan SBOMs and records `{ applied, dropped, kept }` on + * `Scan.scan_metadata.scope_filter`. This spec pins the transparency surface: + * the Components tab's summary band shows an "excluded N" note sourced from + * that telemetry. + * + * S1 — A project whose seeded scan carries scope_filter telemetry shows + * the note with `data-dropped` = the summed per-ecosystem counts + * (seed writes maven=3 + npm=12 → 15), and the per-ecosystem + * breakdown on the title attribute. + * S2 — The note is additive INSIDE the existing summary band (the + * loaded/total copy keeps rendering) — it never replaces the band. + * A scan without telemetry renders no note (pinned by the unit + * tests in tests/unit/parseScopeFilter.test.ts — absence needs no + * second seed here). + * + * Selectors anchor on `data-testid="components-scope-filter-note"` and + * `data-dropped` — never the translated copy — so the suite passes on EN + * and KO alike. + * + * Auth + seed run ONCE for the file (serial describe + shared page): login + * is rate-limited at 5/IP/min, so one login serves every scenario. + * + * Pre-requisites (auto-skip otherwise): docker-compose dev stack up, + * python3 on PATH (seed harness validates). + */ +import { type Page, expect, test } from "@playwright/test"; + +import { AuthHarness } from "../_harness/auth"; +import { PortalPage } from "../_harness/PortalPage"; +import { seedE2eUser, type SeedSummary } from "../_harness/seed"; + +const FILTERED_PROJECT = "ci-scope-filter"; +const EXPECTED_DROPPED = 15; // seed writes {maven: 3, npm: 12} + +let sharedPage: Page; +let seedFailed = false; +let seed: SeedSummary; + +test.describe.serial("@components scope-filter summary note", () => { + test.beforeAll(async ({ browser }) => { + sharedPage = await browser.newPage(); + try { + seed = seedE2eUser({ + projectNames: [FILTERED_PROJECT], + withScan: true, + componentCount: 5, + }); + } catch { + seedFailed = true; + return; + } + const auth = new AuthHarness(sharedPage); + await auth.clearAuthState(); + await auth.gotoLogin(); + await auth.login(seed.email, seed.password); + }); + + test.afterAll(async () => { + await sharedPage?.close(); + }); + + test("S1) summary band shows the excluded-components note with the summed count", async () => { + test.skip(seedFailed, "seed failed — dev stack not reachable"); + const portal = new PortalPage(sharedPage); + await portal.gotoProjects(); + await portal.openProjectDetail(FILTERED_PROJECT); + await portal.selectTab("components"); + + const note = sharedPage.getByTestId("components-scope-filter-note"); + await expect(note).toBeVisible(); + await expect(note).toHaveAttribute("data-dropped", String(EXPECTED_DROPPED)); + // The per-ecosystem breakdown rides the title attribute for operators. + await expect(note).toHaveAttribute("title", /maven: 3/); + await expect(note).toHaveAttribute("title", /npm: 12/); + }); + + test("S2) the summary band itself renders regardless (note is additive)", async () => { + test.skip(seedFailed, "seed failed — dev stack not reachable"); + // The note must live INSIDE the existing summary band, not replace it — + // the loaded/total copy stays the band's first child. + const band = sharedPage.getByTestId("components-summary"); + await expect(band).toBeVisible(); + await expect( + band.getByTestId("components-scope-filter-note"), + ).toBeVisible(); + }); +}); diff --git a/apps/frontend/tests/unit/parseScopeFilter.test.ts b/apps/frontend/tests/unit/parseScopeFilter.test.ts new file mode 100644 index 00000000..5776d599 --- /dev/null +++ b/apps/frontend/tests/unit/parseScopeFilter.test.ts @@ -0,0 +1,54 @@ +/** + * parseScopeFilter — Phase K (PR K-2). + * + * The scope-filter telemetry blob is worker-written JSONB surfaced verbatim + * through `ScanPublic.metadata`; the parser must treat every level as + * untrusted and collapse every "nothing to show" shape to `null` so the + * summary-band note renders only on a positive drop count. + */ +import { describe, expect, it } from "vitest"; + +import { parseScopeFilter } from "@/features/projects/api/useScanScopeFilter"; + +describe("parseScopeFilter", () => { + it("returns dropped counts and their total for a well-formed blob", () => { + const result = parseScopeFilter({ + scope_filter: { applied: true, dropped: { maven: 3, npm: 12 }, kept: 42 }, + }); + expect(result).toEqual({ dropped: { maven: 3, npm: 12 }, totalDropped: 15 }); + }); + + it("returns null when metadata is undefined", () => { + expect(parseScopeFilter(undefined)).toBeNull(); + }); + + it("returns null when the scan carries no scope_filter key (pre-Phase-K)", () => { + expect(parseScopeFilter({ seeded: true })).toBeNull(); + }); + + it("returns null when nothing was dropped (filter ran, found nothing)", () => { + expect( + parseScopeFilter({ scope_filter: { applied: true, dropped: {}, kept: 10 } }), + ).toBeNull(); + }); + + it("ignores zero, negative and non-numeric counts", () => { + expect( + parseScopeFilter({ + scope_filter: { dropped: { maven: 0, npm: -2, weird: "5" } }, + }), + ).toBeNull(); + expect( + parseScopeFilter({ + scope_filter: { dropped: { maven: 2, weird: "5", broken: NaN } }, + }), + ).toEqual({ dropped: { maven: 2 }, totalDropped: 2 }); + }); + + it("tolerates hostile non-object shapes at every level", () => { + expect(parseScopeFilter({ scope_filter: "not-an-object" })).toBeNull(); + expect(parseScopeFilter({ scope_filter: null })).toBeNull(); + expect(parseScopeFilter({ scope_filter: { dropped: [1, 2] } })).toBeNull(); + expect(parseScopeFilter({ scope_filter: { dropped: null } })).toBeNull(); + }); +}); From 7830dec3accacc45fdd3f0ae462cd69716ff006f Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 00:42:24 +0900 Subject: [PATCH 03/10] feat(scan): survive Podfile repos and fill pods in from Podfile.lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a Podfile present and no pod CLI (the worker ships no Ruby/CocoaPods toolchain), cdxgen's cocoapods cataloger throws a TypeError on the undefined pod stdout and aborts the whole SBOM stage — a terminal CdxgenFailed for any iOS repo. Mirror BomLens #347 in two halves: - Crash guard: pass --exclude-type cocoapods when a Podfile exists within depth 3 (Pods/ copies ignored; bounded glob, not BomLens' unbounded find). Applied in both the in-process adapter (integrations/cdxgen.py) and the sidecar script (build_prep_source.sh), cross-referenced to stay in sync. - Fill-in: integrations/cocoapods_lockfile.py hand-parses Podfile.lock's PODS: block (npm_lockfile discipline: never raises, MAX_PODS cap, name-to-ref guard so edges never point at phantom refs, self-edges dropped) and merges components + dependency graph into the SBOM before the scope filter, copy-then-commit like the filter so disk and memory never diverge. Deliberate divergence from BomLens: no syft — the PODS: block already carries names, pinned versions and edges. Subspecs (Moya/Core) percent-encode into the purl; dependency_scope stays NULL (Podfile.lock has no runtime/test signal). - SPM: repos committing only Package.resolved now detect as swift, and the sidecar skips swift package resolve over a committed lockfile (it is the resolved truth; re-resolving hits the network). Verified offline: cdxgen 12.3.3 without a swift toolchain parses Package.resolved directly (the captured real_cyclonedx_swift.json fixture is that verification run). - Fixtures: BomLens' real pod-install Podfile.lock (Moya -> Moya/Core -> Alamofire — subspecs, trailing-colon parents, constraint-only entries). --- CHANGELOG.md | 7 + apps/backend/integrations/cdxgen.py | 30 ++- .../integrations/cocoapods_lockfile.py | 251 ++++++++++++++++++ .../scan_executor/build_prep_source.sh | 22 +- .../scan_executor/source_detect.py | 6 +- apps/backend/tasks/scan_source.py | 66 ++++- .../tests/fixtures/cocoapods/Podfile.lock | 12 + .../fixtures/sbom/real_cyclonedx_swift.json | 1 + .../scan_executor/test_source_detect.py | 1 + .../test_cdxgen_cocoapods_exclude.py | 113 ++++++++ .../integrations/test_cocoapods_lockfile.py | 234 ++++++++++++++++ .../tasks/test_scan_source_cocoapods_merge.py | 194 ++++++++++++++ .../tasks/test_scan_source_scope_filter.py | 6 + docs-site/docs/user-guide/scans.md | 21 ++ .../current/user-guide/scans.md | 20 ++ 15 files changed, 977 insertions(+), 7 deletions(-) create mode 100644 apps/backend/integrations/cocoapods_lockfile.py create mode 100644 apps/backend/tests/fixtures/cocoapods/Podfile.lock create mode 100644 apps/backend/tests/fixtures/sbom/real_cyclonedx_swift.json create mode 100644 apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py create mode 100644 apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py create mode 100644 apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fd15e179..28a937a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- **iOS CocoaPods/SPM lockfile scanning.** A `Podfile` used to crash the + whole source scan (cdxgen's cocoapods cataloger throws without the `pod` + CLI). The scanner now excludes that cataloger and reconstructs pods — + components AND dependency graph, subspecs included — offline from the + committed `Podfile.lock`. Repos with only a committed `Package.resolved` + now route to the swift environment, and the sidecar executors no longer + re-run `swift package resolve` over a committed lockfile. - **Runtime-scope SBOM filtering (default ON).** Source scans now drop non-deployable dependencies from the cdxgen SBOM before persist, signing and vulnerability matching: Maven `test`/`provided` nodes (cdxgen scope tags diff --git a/apps/backend/integrations/cdxgen.py b/apps/backend/integrations/cdxgen.py index 612da9d5..d0791ad6 100644 --- a/apps/backend/integrations/cdxgen.py +++ b/apps/backend/integrations/cdxgen.py @@ -97,6 +97,24 @@ class CdxgenResult: sbom: dict[str, Any] +def _podfile_present(source_dir: Path) -> bool: + """True when a ``Podfile`` exists within depth 3 of the source root. + + Depth-bounded on purpose (mirrors ``source_detect._has_android_manifest``): + BomLens' unbounded ``find .`` over an attacker-controlled tree is a DoS + surface. ``Pods/`` copies are ignored — ``Pods/Manifest.lock`` trees ship + their own Podfiles that do not make the project a CocoaPods root. + """ + if (source_dir / "Podfile").is_file(): + return True + for pattern in ("*/Podfile", "*/*/Podfile"): + for candidate in source_dir.glob(pattern): + relative_parts = candidate.relative_to(source_dir).parts + if candidate.is_file() and "Pods" not in relative_parts: + return True + return False + + # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @@ -166,8 +184,18 @@ def run_cdxgen( str(sbom_path), "--spec-version", resolved_spec, - str(source_dir), ] + if _podfile_present(source_dir): + # Phase L — cdxgen's cocoapods cataloger does NOT skip when the `pod` + # CLI is absent (the worker ships no Ruby/CocoaPods toolchain): it + # throws a TypeError on the undefined `pod` stdout and aborts the + # WHOLE stage-1 scan. Excluding the type keeps the rest of the scan + # alive; integrations/cocoapods_lockfile.py fills the pods back in + # from Podfile.lock after cdxgen (see _merge_cocoapods_components in + # tasks/scan_source.py). The sidecar executors carry the same guard + # in scan_executor/build_prep_source.sh — keep both in sync. + cmd += ["--exclude-type", "cocoapods"] + cmd.append(str(source_dir)) env = _build_cdxgen_env(source_dir=source_dir, output_dir=output_dir, verbose=verbose) if resolved_fetch: # cdxgen resolves component licenses when FETCH_LICENSE is truthy (env, diff --git a/apps/backend/integrations/cocoapods_lockfile.py b/apps/backend/integrations/cocoapods_lockfile.py new file mode 100644 index 00000000..48469043 --- /dev/null +++ b/apps/backend/integrations/cocoapods_lockfile.py @@ -0,0 +1,251 @@ +""" +CocoaPods lockfile parser — iOS component fill-in for cdxgen (Phase L). + +cdxgen's cocoapods cataloger requires the ``pod`` CLI; when a ``Podfile`` is +present and ``pod`` is not (the TRUSCA worker ships no Ruby/CocoaPods +toolchain), the cataloger throws instead of skipping and aborts the whole +SBOM stage. The crash half of the fix passes ``--exclude-type cocoapods`` to +cdxgen (see :mod:`integrations.cdxgen`); this module is the fill-in half — +it reconstructs the pod set and its dependency graph offline from the +committed ``Podfile.lock``, whose ``PODS:`` block is the fully-resolved +truth (direct + transitive, pinned versions, sub-dependency lists). + +Port of BomLens ``docker/lib/parse-podfile-lock.py`` + the component/merge +conventions of ``identify-cocoapods.sh``, with one deliberate difference: +BomLens parses components out of syft output and only reconstructs edges by +hand; TRUSCA's worker ships no syft, and the ``PODS:`` block already carries +names + pinned versions, so this parser produces both halves itself (no new +tool dependency). + +Why parse by hand instead of a YAML lib: Podfile.lock's ``PODS:`` block has a +fixed two-space layout — a top-level `` - Name (ver)`` entry, optionally +followed by four-space `` - SubName (constraint)`` children — and that is +all we need. A YAML dependency for two regexes is not worth its supply-chain +surface. + +Trust boundary +-------------- +``Podfile.lock`` is **attacker-controlled** (the repo author ships any text). +The parser mirrors :mod:`integrations.npm_lockfile`'s discipline: + + * never raises — absent/undecodable/empty lockfiles return ``None``; + * caps parsed pods at ``MAX_PODS`` to bound work; + * edges are emitted only between pods that exist in the parsed set + (name→ref guard), self-edges dropped — a hostile sub-dependency name can + never mint a phantom ref; + * subspec names (``Moya/Core``) are percent-encoded into the purl name + segment so the emitted purl stays spec-valid. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any +from urllib.parse import quote + +import structlog + +log = structlog.get_logger("integrations.cocoapods_lockfile") + +# Defensive cap on PODS entries. Real Podfile.locks top out in the hundreds; +# a hostile file could declare millions of synthetic lines. +MAX_PODS = 20_000 + +# " - Alamofire (5.8.1)" / " - Moya (15.0.0):" (trailing colon when children +# follow). Version group optional — a malformed entry without one is skipped. +_POD_RE = re.compile(r"^ - (?P.+?)(?: \((?P[^)]*)\))?:?\s*$") +# " - Moya/Core (= 15.0.0)" — name only; the constraint is not a version +# (the sub-dependency's pinned version comes from its own top-level entry). +_SUB_RE = re.compile(r"^ - (?P.+?)(?: \([^)]*\))?\s*$") + +# Provenance property stamped on every emitted component (raw_data-visible +# after persist, mirrors the scanoss/npm_lockfile source-marker family). +IDENTIFIED_BY_PROPERTY = {"name": "trusca:identifiedBy", "value": "podfile_lock"} + + +@dataclass(frozen=True) +class CocoapodsLockfileData: + """Parsed view of a ``Podfile.lock``'s ``PODS:`` block. + + Attributes: + pods: pod name → pinned version (subspecs like ``Moya/Core`` are + their own entries, pinned at the parent's version). + edges: pod name → sub-dependency names (names only; resolved to purls + at synthesis time through the ``pods`` map so every edge points + at a real emitted component). + """ + + pods: dict[str, str] = field(default_factory=dict) + edges: dict[str, set[str]] = field(default_factory=dict) + + def purl_for(self, name: str) -> str: + """CycloneDX purl for a pod name (subspec ``/`` percent-encoded).""" + version = self.pods[name] + return f"pkg:cocoapods/{quote(name, safe='')}@{version}" + + def components(self) -> list[dict[str, Any]]: + """cdxgen-shaped component dicts for every parsed pod.""" + out: list[dict[str, Any]] = [] + for name in sorted(self.pods): + purl = self.purl_for(name) + out.append( + { + "type": "library", + "name": name, + "version": self.pods[name], + "purl": purl, + "bom-ref": purl, + "properties": [dict(IDENTIFIED_BY_PROPERTY)], + } + ) + return out + + def synthesize_cdxgen_dependencies(self) -> list[dict[str, Any]]: + """CycloneDX ``dependencies`` entries for pods with sub-dependencies. + + Same output shape as + :meth:`integrations.npm_lockfile.NpmLockfileData.synthesize_cdxgen_dependencies` + so the graph persist path ingests it unchanged. Unknown names are + skipped and self-edges dropped (BomLens name2ref guard). + """ + out: list[dict[str, Any]] = [] + for name in sorted(self.edges): + if name not in self.pods: + continue + ref = self.purl_for(name) + depends = sorted( + { + self.purl_for(sub) + for sub in self.edges[name] + if sub in self.pods and sub != name + } + ) + if depends: + out.append({"ref": ref, "dependsOn": depends}) + return out + + +def read_podfile_lock(source_dir: Path) -> CocoapodsLockfileData | None: + """Parse ``/Podfile.lock``; ``None`` on absence or any error. + + Root-only by design (matches the npm lockfile reader): a nested-only + Podfile.lock still gets the cdxgen crash fix, just no pod fill-in — an + info line flags it for future extension if real repos demand it. + """ + lock_path = source_dir / "Podfile.lock" + if not lock_path.is_file(): + for pattern in ("*/Podfile.lock", "*/*/Podfile.lock"): + for nested in source_dir.glob(pattern): + if "Pods" not in nested.parts: + log.info( + "cocoapods_lockfile_not_at_root", found=str(nested) + ) + break + return None + try: + lines = lock_path.read_text( + encoding="utf-8", errors="replace" + ).splitlines() + except OSError as exc: + log.warning( + "cocoapods_lockfile_read_failed", + path=str(lock_path), + error=str(exc), + ) + return None + + pods: dict[str, str] = {} + edges: dict[str, set[str]] = {} + in_pods = False + current: str | None = None + parsed = 0 + for line in lines: + if not in_pods: + if line.rstrip() == "PODS:": + in_pods = True + continue + # The PODS block ends at the next top-level key (a non-indented, + # non-empty line such as ``DEPENDENCIES:``). + if line and not line.startswith(" "): + break + pod_match = _POD_RE.match(line) + if pod_match: + parsed += 1 + if parsed > MAX_PODS: + log.warning("cocoapods_lockfile_pod_cap_exceeded", limit=MAX_PODS) + break + name = pod_match.group("name").strip() + version = (pod_match.group("ver") or "").strip() + if not name or not version: + current = None # malformed/versionless entry — skip its subs too + continue + pods[name] = version + edges.setdefault(name, set()) + current = name + continue + sub_match = _SUB_RE.match(line) + if sub_match and current is not None: + edges[current].add(sub_match.group("name").strip()) + + if not pods: + return None + return CocoapodsLockfileData(pods=pods, edges=edges) + + +def merge_into_sbom(sbom: dict[str, Any], data: CocoapodsLockfileData) -> int: + """Union the pod components + edges into ``sbom``; returns merged count. + + Guarded no-op (returns 0) when the document already carries any + ``pkg:cocoapods/`` component — e.g. a future pod-capable cdxgen image — + so pods are never double-counted (BomLens ``identify-cocoapods`` guard). + Never raises. + """ + try: + components = sbom.setdefault("components", []) + if not isinstance(components, list): + return 0 + for existing in components: + if isinstance(existing, dict): + purl = existing.get("purl") + if isinstance(purl, str) and purl.startswith("pkg:cocoapods/"): + log.info("cocoapods_merge_skipped_existing_pods") + return 0 + pod_components = data.components() + components.extend(pod_components) + + dependencies = sbom.setdefault("dependencies", []) + if isinstance(dependencies, list): + dependencies.extend(data.synthesize_cdxgen_dependencies()) + + metadata = sbom.setdefault("metadata", {}) + if isinstance(metadata, dict): + properties = metadata.setdefault("properties", []) + if isinstance(properties, list): + properties[:] = [ + prop + for prop in properties + if not ( + isinstance(prop, dict) + and prop.get("name") == "trusca:cocoapods" + ) + ] + properties.append( + { + "name": "trusca:cocoapods", + "value": f"podfile_lock:{len(pod_components)}", + } + ) + return len(pod_components) + except Exception: # noqa: BLE001 — fill-in must never break a scan + log.warning("cocoapods_merge_failed", exc_info=True) + return 0 + + +__all__ = [ + "MAX_PODS", + "CocoapodsLockfileData", + "merge_into_sbom", + "read_podfile_lock", +] diff --git a/apps/backend/integrations/scan_executor/build_prep_source.sh b/apps/backend/integrations/scan_executor/build_prep_source.sh index ce95ca38..4d220078 100644 --- a/apps/backend/integrations/scan_executor/build_prep_source.sh +++ b/apps/backend/integrations/scan_executor/build_prep_source.sh @@ -69,7 +69,10 @@ if { ls ./*.csproj >/dev/null 2>&1 || ls ./*.sln >/dev/null 2>&1; } && command - fi # Swift / SPM — resolve generates Package.resolved so cdxgen sees the graph. -if [ -f Package.swift ] && command -v swift >/dev/null 2>&1; then +# Skipped when Package.resolved is already committed: the committed lockfile +# IS the resolved truth, and re-resolving hits the network and can drag in +# unrelated tooling / "unspecified" versions (BomLens build-prep parity). +if [ -f Package.swift ] && [ ! -f Package.resolved ] && command -v swift >/dev/null 2>&1; then log "swift package resolve" swift package resolve >/dev/null 2>&1 || true fi @@ -79,15 +82,26 @@ fi # transitive graph itself (a separate step was redundant + noisy, per BomLens). # --- locate cdxgen (path differs per image) and generate the SBOM --- +# Phase L: with a Podfile present and no `pod` CLI, cdxgen's cocoapods +# cataloger throws (TypeError on undefined stdout) and kills the whole run — +# exclude the type; the worker fills pods back in from Podfile.lock +# (integrations/cocoapods_lockfile.py). The in-process adapter carries the +# same guard in integrations/cdxgen.py (_podfile_present) — keep both in sync. +# Depth-bounded find mirrors the adapter's depth-3 glob; Pods/ copies ignored. +set -- -r --no-validate --spec-version "$SPEC" -o "$OUT" +if find "$SRC" -maxdepth 3 -name Podfile -type f -not -path '*/Pods/*' 2>/dev/null | grep -q .; then + log "Podfile detected — cdxgen --exclude-type cocoapods (pod CLI absent in image)" + set -- "$@" --exclude-type cocoapods +fi if command -v cdxgen >/dev/null 2>&1; then log "cdxgen (PATH)" - cdxgen -r --no-validate --spec-version "$SPEC" -o "$OUT" "$SRC" + cdxgen "$@" "$SRC" elif [ -f /opt/cdxgen/bin/cdxgen.js ]; then log "cdxgen (/opt/cdxgen/bin/cdxgen.js)" - node /opt/cdxgen/bin/cdxgen.js -r --no-validate --spec-version "$SPEC" -o "$OUT" "$SRC" + node /opt/cdxgen/bin/cdxgen.js "$@" "$SRC" elif [ -f /opt/bin/cdxgen ]; then log "cdxgen (/opt/bin/cdxgen)" - /opt/bin/cdxgen -r --no-validate --spec-version "$SPEC" -o "$OUT" "$SRC" + /opt/bin/cdxgen "$@" "$SRC" else echo "[build-prep] ERROR: cdxgen not found in image" >&2 exit 1 diff --git a/apps/backend/integrations/scan_executor/source_detect.py b/apps/backend/integrations/scan_executor/source_detect.py index 7b986962..c6d11647 100644 --- a/apps/backend/integrations/scan_executor/source_detect.py +++ b/apps/backend/integrations/scan_executor/source_detect.py @@ -138,7 +138,11 @@ def _is_android(source_dir: Path) -> bool: def _is_swift(source_dir: Path) -> bool: - if any((source_dir / name).is_file() for name in ("Package.swift", "Podfile", "Podfile.lock")): + # Package.resolved: a repo may commit only the resolved SPM lockfile + # (cdxgen parses it offline, both v1 object.pins and v2 pins — verified + # against the pinned cdxgen 12.3.3, Phase L). + swift_markers = ("Package.swift", "Package.resolved", "Podfile", "Podfile.lock") + if any((source_dir / name).is_file() for name in swift_markers): return True return any(source_dir.glob("*.xcodeproj")) or any(source_dir.glob("*.xcworkspace")) diff --git a/apps/backend/tasks/scan_source.py b/apps/backend/tasks/scan_source.py index e687fd9b..4f70cbb2 100644 --- a/apps/backend/tasks/scan_source.py +++ b/apps/backend/tasks/scan_source.py @@ -87,8 +87,8 @@ from core.url_guard import GitUrlValidationError, validate_git_url_with_ip from integrations import attestation as attestation_builder from integrations import cdxgen as cdxgen_adapter +from integrations import cocoapods_lockfile, sbom_scope_filter, scan_executor from integrations import cosign as cosign_adapter -from integrations import sbom_scope_filter, scan_executor from integrations import scancode as scancode_adapter from integrations import scanoss as scanoss_adapter from integrations._size_guard import enforce_jsonb_row_size_limit @@ -398,6 +398,17 @@ def _run_pipeline( sbom_path=gen_result.sbom_path, sbom=gen_result.sbom ) + # Stage 3.2 — CocoaPods lockfile fill-in (Phase L). cdxgen ran with + # --exclude-type cocoapods when a Podfile was present (its cataloger + # crashes without the `pod` CLI — see integrations/cdxgen.py), so the + # pods are reconstructed offline from Podfile.lock and merged here. + # MUST precede the scope filter so the filter's counts and the + # trusca:scope_filter property describe the FINAL document (cocoapods + # purls pass both keep-predicates untouched). Best-effort, never fatal. + _merge_cocoapods_components( + scan_uuid=scan_uuid, cdxgen_result=cdxgen_result, source_dir=source_dir + ) + # Stage 3.25 — runtime-scope post-filter (Phase K). MUST run before the # artifact persist, cosign signing, SCANOSS and Trivy below so every # downstream consumer (persisted components, the signed/downloadable @@ -1148,6 +1159,59 @@ def _detect_and_record_env(scan_uuid: uuid.UUID, source_dir: Path) -> str: return detected_env +def _merge_cocoapods_components( + *, + scan_uuid: uuid.UUID, + cdxgen_result: cdxgen_adapter.CdxgenResult, + source_dir: Path, +) -> None: + """Fill CocoaPods components back into the SBOM from Podfile.lock (Phase L). + + Same copy-then-commit shape as ``_apply_scope_filter``: merge into a deep + copy, rewrite the on-disk file atomically, and only then swap the + in-memory document — persist, signing and Trivy can never see diverging + documents. Best-effort throughout; ``dependency_scope`` stays NULL for + pods (Podfile.lock carries no runtime/test distinction). + """ + try: + lock_data = cocoapods_lockfile.read_podfile_lock(source_dir) + if lock_data is None: + return + working = copy.deepcopy(cdxgen_result.sbom) + merged = cocoapods_lockfile.merge_into_sbom(working, lock_data) + if merged <= 0: + return + if not sbom_scope_filter.rewrite_sbom_file( + cdxgen_result.sbom_path, working + ): + return + cdxgen_result.sbom.clear() + cdxgen_result.sbom.update(working) + log.info( + "cocoapods_lockfile_merged", + scan_id=str(scan_uuid), + merged=merged, + ) + try: + with sync_session_scope() as session: + scan = session.get(Scan, scan_uuid) + if scan is not None: + meta = dict(scan.scan_metadata or {}) + meta["cocoapods"] = {"merged": merged} + scan.scan_metadata = meta + session.commit() + except Exception: # noqa: BLE001 — telemetry is best-effort + log.warning( + "cocoapods_merge_persist_failed", + scan_id=str(scan_uuid), + exc_info=True, + ) + except Exception: # noqa: BLE001 — the fill-in must never fail the scan + log.warning( + "cocoapods_merge_stage_failed", scan_id=str(scan_uuid), exc_info=True + ) + + def _apply_scope_filter( *, scan_uuid: uuid.UUID, diff --git a/apps/backend/tests/fixtures/cocoapods/Podfile.lock b/apps/backend/tests/fixtures/cocoapods/Podfile.lock new file mode 100644 index 00000000..5979398a --- /dev/null +++ b/apps/backend/tests/fixtures/cocoapods/Podfile.lock @@ -0,0 +1,12 @@ +PODS: + - Alamofire (5.8.1) + - Moya (15.0.0): + - Moya/Core (= 15.0.0) + - Moya/Core (15.0.0): + - Alamofire (~> 5.0) +DEPENDENCIES: + - Moya +SPEC CHECKSUMS: + Alamofire: f3b09a368f1582ab751b3fff5460276e0d2cf5c9 + Moya: 5b45dacb75adcbf9640cf7c6ef742a25e8a1a3f7 +COCOAPODS: 1.12.1 diff --git a/apps/backend/tests/fixtures/sbom/real_cyclonedx_swift.json b/apps/backend/tests/fixtures/sbom/real_cyclonedx_swift.json new file mode 100644 index 00000000..2fd8c78e --- /dev/null +++ b/apps/backend/tests/fixtures/sbom/real_cyclonedx_swift.json @@ -0,0 +1 @@ +{"bomFormat":"CycloneDX","specVersion":"1.5","serialNumber":"urn:uuid:fded1ba0-d6e4-4b2d-a77b-767d9ce9658c","version":1,"metadata":{"timestamp":"2026-07-10T15:32:31Z","tools":{"components":[{"group":"@cyclonedx","name":"cdxgen","version":"12.3.3","purl":"pkg:npm/%40cyclonedx/cdxgen@12.3.3","type":"application","bom-ref":"pkg:npm/@cyclonedx/cdxgen@12.3.3","author":"OWASP Foundation","publisher":"OWASP Foundation"}]},"authors":[{"name":"OWASP Foundation"}],"lifecycles":[{"phase":"build"}],"component":{"group":"","name":"ios-spm-clean","version":"latest","type":"application","bom-ref":"pkg:swift/ios-spm-clean@latest","purl":"pkg:swift/ios-spm-clean@latest"},"properties":[{"name":"cdx:bom:componentTypes","value":"swift"},{"name":"cdx:bom:componentNamespaces","value":"github.com/Alamofire\\ngithub.com/apple"},{"name":"cdx:bom:componentSrcFiles","value":"Package.resolved\\nPackage.swift"}]},"components":[{"group":"github.com/Alamofire","name":"Alamofire","version":"5.8.1","purl":"pkg:swift/github.com/Alamofire/Alamofire@5.8.1","externalReferences":[{"type":"vcs","url":"https://github.com/Alamofire/Alamofire.git"}],"type":"library","bom-ref":"pkg:swift/github.com/Alamofire/Alamofire@5.8.1","properties":[{"name":"SrcFile","value":"Package.resolved"},{"name":"cdx:swift:localCheckoutPath","value":".build/checkouts/Alamofire"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/ios-spm-clean/Package.resolved"}]}}},{"group":"github.com/apple","name":"swift-log","version":"1.5.3","purl":"pkg:swift/github.com/apple/swift-log@1.5.3","externalReferences":[{"type":"vcs","url":"https://github.com/apple/swift-log.git"}],"type":"library","bom-ref":"pkg:swift/github.com/apple/swift-log@1.5.3","properties":[{"name":"SrcFile","value":"Package.resolved"},{"name":"cdx:swift:localCheckoutPath","value":".build/checkouts/swift-log"}],"evidence":{"identity":{"field":"purl","confidence":1,"methods":[{"technique":"manifest-analysis","confidence":1,"value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/ios-spm-clean/Package.resolved"}]}}},{"group":"","name":"App","version":"unspecified","purl":"pkg:swift/App@unspecified","type":"application","bom-ref":"pkg:swift/App@unspecified","properties":[{"name":"SrcPath","value":"/private/tmp/claude-296035173/-Users-1112821-projects-trusca/ae25df99-4ff9-47bc-b4ca-38b0661895bb/scratchpad/fixtures/ios-spm-clean"},{"name":"SrcFile","value":"Package.swift"}]}],"services":[],"dependencies":[{"ref":"pkg:swift/github.com/Alamofire/Alamofire@5.8.1","dependsOn":[]},{"ref":"pkg:swift/github.com/apple/swift-log@1.5.3","dependsOn":[]},{"ref":"pkg:swift/App@unspecified","dependsOn":["pkg:swift/github.com/Alamofire/Alamofire@5.8.1","pkg:swift/github.com/apple/swift-log@1.5.3"]},{"ref":"pkg:swift/ios-spm-clean@latest","dependsOn":[]}]} \ No newline at end of file diff --git a/apps/backend/tests/unit/integrations/scan_executor/test_source_detect.py b/apps/backend/tests/unit/integrations/scan_executor/test_source_detect.py index a0c24c0c..77669403 100644 --- a/apps/backend/tests/unit/integrations/scan_executor/test_source_detect.py +++ b/apps/backend/tests/unit/integrations/scan_executor/test_source_detect.py @@ -50,6 +50,7 @@ def _touch(root: Path, *relpaths: str) -> Path: (["conanfile.py"], "cpp"), (["vcpkg.json"], "cpp"), (["Package.swift"], "swift"), + (["Package.resolved"], "swift"), (["Podfile"], "swift"), (["Podfile.lock"], "swift"), ], diff --git a/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py b/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py new file mode 100644 index 00000000..0592d4f8 --- /dev/null +++ b/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py @@ -0,0 +1,113 @@ +"""Unit tests — cdxgen CocoaPods crash guard (Phase L). + +cdxgen's cocoapods cataloger does not skip when the ``pod`` CLI is absent — +it throws a TypeError on the undefined ``pod`` stdout and aborts the whole +stage-1 run (a terminal ``CdxgenFailed``). The guard passes +``--exclude-type cocoapods`` whenever a Podfile is present so the rest of the +scan survives; ``integrations/cocoapods_lockfile.py`` fills the pods back in. + +Pinned here: + * ``_podfile_present`` — depth-3 bounded detection, ``Pods/`` copies + ignored (a vendored Pods tree must not flag a non-CocoaPods root). + * ``run_cdxgen`` cmd assembly — the exclude flag appears exactly when a + Podfile is detected, and the source dir stays the LAST argument. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +from integrations import cdxgen + +# --------------------------------------------------------------------------- +# _podfile_present +# --------------------------------------------------------------------------- + + +def test_podfile_at_root_detected(tmp_path: Path) -> None: + (tmp_path / "Podfile").write_text("platform :ios, '15.0'\n", encoding="utf-8") + assert cdxgen._podfile_present(tmp_path) is True + + +@pytest.mark.parametrize("depth", ["app", "app/ios"]) +def test_podfile_nested_within_depth_three_detected( + tmp_path: Path, depth: str +) -> None: + nested = tmp_path / depth + nested.mkdir(parents=True) + (nested / "Podfile").write_text("platform :ios\n", encoding="utf-8") + assert cdxgen._podfile_present(tmp_path) is True + + +def test_podfile_deeper_than_three_levels_not_detected(tmp_path: Path) -> None: + nested = tmp_path / "a" / "b" / "c" + nested.mkdir(parents=True) + (nested / "Podfile").write_text("platform :ios\n", encoding="utf-8") + assert cdxgen._podfile_present(tmp_path) is False # DoS-bounded by design + + +def test_pods_directory_copy_ignored(tmp_path: Path) -> None: + pods = tmp_path / "Pods" + pods.mkdir() + (pods / "Podfile").write_text("vendored copy\n", encoding="utf-8") + assert cdxgen._podfile_present(tmp_path) is False + + +def test_no_podfile_not_detected(tmp_path: Path) -> None: + (tmp_path / "Package.swift").write_text("// spm\n", encoding="utf-8") + assert cdxgen._podfile_present(tmp_path) is False + + +# --------------------------------------------------------------------------- +# run_cdxgen cmd assembly +# --------------------------------------------------------------------------- + + +@pytest.fixture +def captured_cmd(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]: + """Stub the subprocess layer; capture cmd and fake a successful run.""" + calls: list[list[str]] = [] + + def _fake_run(cmd: list[str], **kwargs: Any) -> SimpleNamespace: + calls.append(cmd) + out_path = Path(cmd[cmd.index("-o") + 1]) + out_path.write_text( + json.dumps({"components": [], "dependencies": []}), encoding="utf-8" + ) + return SimpleNamespace(returncode=0, stdout=b"", stderr=b"") + + monkeypatch.setattr(cdxgen, "run_with_line_streaming", _fake_run) + monkeypatch.setattr(cdxgen.shutil, "which", lambda name: f"/usr/bin/{name}") + return calls + + +def test_exclude_type_present_iff_podfile_detected( + tmp_path: Path, captured_cmd: list[list[str]] +) -> None: + source = tmp_path / "src" + source.mkdir() + (source / "Podfile").write_text("platform :ios\n", encoding="utf-8") + cdxgen.run_cdxgen( + source_dir=source, output_dir=tmp_path / "out", backend="real" + ) + cmd = captured_cmd[-1] + exclude_index = cmd.index("--exclude-type") + assert cmd[exclude_index + 1] == "cocoapods" + assert cmd[-1] == str(source) # source dir stays the last argument + + +def test_no_exclude_type_without_podfile( + tmp_path: Path, captured_cmd: list[list[str]] +) -> None: + source = tmp_path / "src" + source.mkdir() + (source / "package.json").write_text("{}", encoding="utf-8") + cdxgen.run_cdxgen( + source_dir=source, output_dir=tmp_path / "out", backend="real" + ) + assert "--exclude-type" not in captured_cmd[-1] diff --git a/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py b/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py new file mode 100644 index 00000000..014d7531 --- /dev/null +++ b/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py @@ -0,0 +1,234 @@ +"""Unit tests — CocoaPods lockfile parser + SBOM merge (Phase L). + +The fixture at ``tests/fixtures/cocoapods/Podfile.lock`` is REAL ``pod +install`` output (vendored from BomLens ``tests/fixtures/ios-cocoapods``, +the file their iOS e2e verifies against): the ``Moya → Moya/Core → +Alamofire`` chain exercises subspec entries, trailing-colon parents, +constraint-only sub-entries and the block terminator (``DEPENDENCIES:``). + +Contract halves: + * parse — names/pinned versions/edges out of the PODS: block; subspec + purl percent-encoding; the block ends at the first non-indented line; + the name→ref guard means an edge can never point at a phantom ref. + * merge — union into a cdxgen SBOM; guarded no-op when pkg:cocoapods/* + already present; provenance property + metadata stamp; never raises. + +Adversarial posture mirrors test_npm_lockfile.py: Podfile.lock is +attacker-controlled, so malformed shapes degrade to None / 0 — never raise. +""" + +from __future__ import annotations + +from pathlib import Path + +from integrations.cocoapods_lockfile import ( + MAX_PODS, + CocoapodsLockfileData, + merge_into_sbom, + read_podfile_lock, +) + +FIXTURE = ( + Path(__file__).resolve().parents[2] / "fixtures" / "cocoapods" / "Podfile.lock" +) + + +def _write_lock(tmp_path: Path, content: str) -> Path: + (tmp_path / "Podfile.lock").write_text(content, encoding="utf-8") + return tmp_path + + +# --------------------------------------------------------------------------- +# Parsing — real fixture +# --------------------------------------------------------------------------- + + +def test_real_fixture_pods_versions_and_edges(tmp_path: Path) -> None: + (tmp_path / "Podfile.lock").write_text( + FIXTURE.read_text(encoding="utf-8"), encoding="utf-8" + ) + data = read_podfile_lock(tmp_path) + assert data is not None + assert data.pods == { + "Alamofire": "5.8.1", + "Moya": "15.0.0", + "Moya/Core": "15.0.0", # subspec pinned at the parent's version + } + # Moya -> Moya/Core (trailing-colon parent), Moya/Core -> Alamofire + # (constraint-only sub-entry "(~> 5.0)" — the constraint is NOT a version). + assert data.edges["Moya"] == {"Moya/Core"} + assert data.edges["Moya/Core"] == {"Alamofire"} + assert data.edges["Alamofire"] == set() + + +def test_real_fixture_components_and_graph_shapes(tmp_path: Path) -> None: + (tmp_path / "Podfile.lock").write_text( + FIXTURE.read_text(encoding="utf-8"), encoding="utf-8" + ) + data = read_podfile_lock(tmp_path) + assert data is not None + + components = data.components() + by_name = {c["name"]: c for c in components} + # Subspec slash percent-encoded in the purl; display name stays raw. + assert by_name["Moya/Core"]["purl"] == "pkg:cocoapods/Moya%2FCore@15.0.0" + assert by_name["Alamofire"]["purl"] == "pkg:cocoapods/Alamofire@5.8.1" + for component in components: + assert component["bom-ref"] == component["purl"] + assert {"name": "trusca:identifiedBy", "value": "podfile_lock"} in component[ + "properties" + ] + + graph = data.synthesize_cdxgen_dependencies() + graph_by_ref = {entry["ref"]: entry["dependsOn"] for entry in graph} + assert graph_by_ref["pkg:cocoapods/Moya@15.0.0"] == [ + "pkg:cocoapods/Moya%2FCore@15.0.0" + ] + assert graph_by_ref["pkg:cocoapods/Moya%2FCore@15.0.0"] == [ + "pkg:cocoapods/Alamofire@5.8.1" + ] + # Leaf pods emit no entry (BomLens prints only pods with sub-deps). + assert "pkg:cocoapods/Alamofire@5.8.1" not in graph_by_ref + + +# --------------------------------------------------------------------------- +# Parsing — block boundaries + guards +# --------------------------------------------------------------------------- + + +def test_pods_block_ends_at_next_top_level_key(tmp_path: Path) -> None: + # A pod-shaped line AFTER the block terminator must not be parsed. + src = _write_lock( + tmp_path, + "PODS:\n - RealPod (1.0.0)\nDEPENDENCIES:\n - RealPod\n" + "SPEC REPOS:\n - GhostPod (9.9.9)\n", + ) + data = read_podfile_lock(src) + assert data is not None + assert set(data.pods) == {"RealPod"} + + +def test_edge_to_unknown_pod_is_skipped(tmp_path: Path) -> None: + # Sub-dependency name that has no top-level PODS entry → no phantom ref. + src = _write_lock( + tmp_path, + "PODS:\n - Parent (1.0.0):\n - MissingChild (~> 2.0)\n", + ) + data = read_podfile_lock(src) + assert data is not None + assert data.edges["Parent"] == {"MissingChild"} + assert data.synthesize_cdxgen_dependencies() == [] # guard drops it + + +def test_self_edge_dropped(tmp_path: Path) -> None: + src = _write_lock( + tmp_path, + "PODS:\n - Loop (1.0.0):\n - Loop (= 1.0.0)\n", + ) + data = read_podfile_lock(src) + assert data is not None + assert data.synthesize_cdxgen_dependencies() == [] + + +def test_versionless_pod_entry_skipped_with_its_subs(tmp_path: Path) -> None: + src = _write_lock( + tmp_path, + "PODS:\n - NoVersion:\n - Child (1.0)\n - Real (2.0.0)\n", + ) + data = read_podfile_lock(src) + assert data is not None + assert set(data.pods) == {"Real"} + + +# --------------------------------------------------------------------------- +# Degradation +# --------------------------------------------------------------------------- + + +def test_absent_lockfile_returns_none(tmp_path: Path) -> None: + assert read_podfile_lock(tmp_path) is None + + +def test_nested_only_lockfile_returns_none(tmp_path: Path) -> None: + nested = tmp_path / "app" + nested.mkdir() + (nested / "Podfile.lock").write_text("PODS:\n - A (1.0)\n", encoding="utf-8") + assert read_podfile_lock(tmp_path) is None # root-only by design + + +def test_pods_dir_copy_not_treated_as_nested_signal(tmp_path: Path) -> None: + pods_dir = tmp_path / "Pods" + pods_dir.mkdir() + (pods_dir / "Podfile.lock").write_text("PODS:\n - A (1.0)\n", encoding="utf-8") + assert read_podfile_lock(tmp_path) is None + + +def test_no_pods_block_returns_none(tmp_path: Path) -> None: + src = _write_lock(tmp_path, "DEPENDENCIES:\n - Something\n") + assert read_podfile_lock(src) is None + + +def test_garbage_content_returns_none(tmp_path: Path) -> None: + src = _write_lock(tmp_path, "\x00\x01 not yaml at all {{{{") + assert read_podfile_lock(src) is None + + +def test_pod_cap_bounds_hostile_lockfile(tmp_path: Path) -> None: + lines = ["PODS:"] + [f" - P{i} (1.0.{i})" for i in range(MAX_PODS + 10)] + src = _write_lock(tmp_path, "\n".join(lines) + "\n") + data = read_podfile_lock(src) + assert data is not None + assert len(data.pods) == MAX_PODS + + +# --------------------------------------------------------------------------- +# merge_into_sbom +# --------------------------------------------------------------------------- + + +def _data() -> CocoapodsLockfileData: + return CocoapodsLockfileData( + pods={"Alamofire": "5.8.1", "Moya": "15.0.0"}, + edges={"Moya": {"Alamofire"}, "Alamofire": set()}, + ) + + +def test_merge_appends_components_edges_and_stamp() -> None: + sbom = { + "components": [{"purl": "pkg:swift/github.com/apple/swift-log@1.5.3"}], + "dependencies": [], + "metadata": {}, + } + merged = merge_into_sbom(sbom, _data()) + assert merged == 2 + purls = {c["purl"] for c in sbom["components"] if isinstance(c, dict)} + assert "pkg:cocoapods/Alamofire@5.8.1" in purls + assert "pkg:cocoapods/Moya@15.0.0" in purls + assert { + "ref": "pkg:cocoapods/Moya@15.0.0", + "dependsOn": ["pkg:cocoapods/Alamofire@5.8.1"], + } in sbom["dependencies"] + assert {"name": "trusca:cocoapods", "value": "podfile_lock:2"} in sbom[ + "metadata" + ]["properties"] + + +def test_merge_noop_when_pods_already_present() -> None: + sbom = { + "components": [{"purl": "pkg:cocoapods/Existing@1.0.0"}], + "dependencies": [], + } + assert merge_into_sbom(sbom, _data()) == 0 + assert len(sbom["components"]) == 1 # untouched + + +def test_merge_tolerates_missing_arrays() -> None: + sbom: dict = {} + merged = merge_into_sbom(sbom, _data()) + assert merged == 2 + assert len(sbom["components"]) == 2 + assert isinstance(sbom["dependencies"], list) + + +def test_merge_never_raises_on_hostile_document() -> None: + assert merge_into_sbom({"components": "not-a-list"}, _data()) == 0 diff --git a/apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py b/apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py new file mode 100644 index 00000000..973830e4 --- /dev/null +++ b/apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py @@ -0,0 +1,194 @@ +"""Unit tests — CocoaPods fill-in pipeline wiring (Phase L). + +Runs the real captured fixtures through ``_merge_cocoapods_components``: +``real_cyclonedx_swift.json`` is genuine cdxgen 12.3.3 output over a +Package.resolved-only tree captured WITHOUT a swift toolchain on PATH (that +capture doubles as the offline-parse verification: cdxgen read the committed +lockfile directly — Alamofire 5.8.1 + swift-log 1.5.3 as pkg:swift purls); +the Podfile.lock is the real ``pod install`` fixture vendored from BomLens. + +Pins: + * copy-then-commit — disk and memory identical after a merge; a failed + disk rewrite leaves both untouched; + * merged pods flow through ``persist_sbom_components`` with + ``dependency_scope`` NULL (Podfile.lock has no runtime/test signal — the + FE renders an em-dash, never a fabricated "Required"); + * no Podfile.lock → full no-op. +""" + +from __future__ import annotations + +import copy +import json +import shutil +import uuid +from contextlib import contextmanager +from pathlib import Path +from typing import Any + +import pytest + +from integrations.cdxgen import CdxgenResult + +FIXTURES = Path(__file__).resolve().parents[2] / "fixtures" +SWIFT_SBOM = FIXTURES / "sbom" / "real_cyclonedx_swift.json" +PODFILE_LOCK = FIXTURES / "cocoapods" / "Podfile.lock" + + +class _FakeComponent: + def __init__(self) -> None: + self.id = uuid.uuid4() + + +class _FakeComponentVersion: + def __init__(self) -> None: + self.id = uuid.uuid4() + + +class _FakeSession: + def __init__(self) -> None: + self.added: list[Any] = [] + + def add(self, row: Any) -> None: + self.added.append(row) + + +@pytest.fixture +def quiet_telemetry(monkeypatch: pytest.MonkeyPatch) -> None: + """Route the helper's scan_metadata write into a no-op session scope.""" + + class _NullSession: + def get(self, *args: Any, **kwargs: Any) -> None: + return None + + def commit(self) -> None: + return None + + @contextmanager + def _scope() -> Any: + yield _NullSession() + + monkeypatch.setattr("tasks.scan_source.sync_session_scope", _scope) + + +def _cdxgen_result(tmp_path: Path) -> CdxgenResult: + sbom = json.loads(SWIFT_SBOM.read_text(encoding="utf-8")) + sbom_path = tmp_path / "bom.json" + sbom_path.write_text(json.dumps(sbom), encoding="utf-8") + return CdxgenResult(sbom_path=sbom_path, sbom=sbom) + + +def _source_dir_with_lock(tmp_path: Path) -> Path: + source = tmp_path / "src" + source.mkdir() + shutil.copy(PODFILE_LOCK, source / "Podfile.lock") + return source + + +def test_merge_updates_disk_and_memory_consistently( + tmp_path: Path, quiet_telemetry: None +) -> None: + from tasks import scan_source + + cdxgen_result = _cdxgen_result(tmp_path) + before_count = len(cdxgen_result.sbom["components"]) + source = _source_dir_with_lock(tmp_path) + + scan_source._merge_cocoapods_components( + scan_uuid=uuid.uuid4(), cdxgen_result=cdxgen_result, source_dir=source + ) + + on_disk = json.loads(cdxgen_result.sbom_path.read_text(encoding="utf-8")) + assert on_disk == cdxgen_result.sbom + purls = { + c.get("purl") + for c in cdxgen_result.sbom["components"] + if isinstance(c, dict) + } + assert "pkg:cocoapods/Alamofire@5.8.1" in purls + assert "pkg:cocoapods/Moya@15.0.0" in purls + assert "pkg:cocoapods/Moya%2FCore@15.0.0" in purls + assert len(cdxgen_result.sbom["components"]) == before_count + 3 + # The original pkg:swift components are untouched (union, not replace). + assert "pkg:swift/github.com/Alamofire/Alamofire@5.8.1" in purls + + +def test_merge_noop_without_podfile_lock( + tmp_path: Path, quiet_telemetry: None +) -> None: + from tasks import scan_source + + cdxgen_result = _cdxgen_result(tmp_path) + original = copy.deepcopy(cdxgen_result.sbom) + source = tmp_path / "src" + source.mkdir() + + scan_source._merge_cocoapods_components( + scan_uuid=uuid.uuid4(), cdxgen_result=cdxgen_result, source_dir=source + ) + + assert cdxgen_result.sbom == original + assert json.loads(cdxgen_result.sbom_path.read_text(encoding="utf-8")) == original + + +def test_merge_rewrite_failure_keeps_memory_and_disk_unmerged( + tmp_path: Path, quiet_telemetry: None, monkeypatch: pytest.MonkeyPatch +) -> None: + from tasks import scan_source + + monkeypatch.setattr( + scan_source.sbom_scope_filter, "rewrite_sbom_file", lambda path, sbom: False + ) + cdxgen_result = _cdxgen_result(tmp_path) + original = copy.deepcopy(cdxgen_result.sbom) + source = _source_dir_with_lock(tmp_path) + + scan_source._merge_cocoapods_components( + scan_uuid=uuid.uuid4(), cdxgen_result=cdxgen_result, source_dir=source + ) + + assert cdxgen_result.sbom == original + assert json.loads(cdxgen_result.sbom_path.read_text(encoding="utf-8")) == original + + +def test_merged_pods_persist_with_null_dependency_scope( + tmp_path: Path, quiet_telemetry: None, monkeypatch: pytest.MonkeyPatch +) -> None: + from models import ScanComponent + from tasks import scan_source + + monkeypatch.setattr( + "tasks.scan_source._get_or_create_component", + lambda session, *, purl, name, package_type: _FakeComponent(), + ) + monkeypatch.setattr( + "tasks.scan_source._get_or_create_component_version", + lambda session, *, component, version, purl_with_version: _FakeComponentVersion(), + ) + monkeypatch.setattr( + "tasks.scan_source._persist_component_licenses", + lambda session, *, scan_uuid, component_version_id, cdxgen_component, purl: None, + ) + monkeypatch.setattr( + "tasks.scan_source._persist_dependency_graph", + lambda session, **kwargs: None, + ) + + cdxgen_result = _cdxgen_result(tmp_path) + source = _source_dir_with_lock(tmp_path) + scan_source._merge_cocoapods_components( + scan_uuid=uuid.uuid4(), cdxgen_result=cdxgen_result, source_dir=source + ) + + session = _FakeSession() + scan_source.persist_sbom_components( + session, scan_uuid=uuid.uuid4(), sbom=cdxgen_result.sbom + ) + pod_rows = [ + r + for r in session.added + if isinstance(r, ScanComponent) + and (r.dependency_path or "").startswith("pkg:cocoapods/") + ] + assert len(pod_rows) == 3 + assert all(r.dependency_scope is None for r in pod_rows) diff --git a/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py index a4893605..b83b9df6 100644 --- a/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py +++ b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py @@ -334,6 +334,12 @@ def test_pipeline_filters_before_artifact_persist_and_signing() -> None: order = _call_order("_run_pipeline", Path(scan_source_module.__file__)) assert order.index("_apply_scope_filter") < order.index("_persist_artifact") assert order.index("_apply_scope_filter") < order.index("_sign_sbom") + # Phase L — the CocoaPods fill-in must precede the scope filter so the + # filter's counts + trusca:scope_filter property describe the FINAL + # document (and both precede the artifact persist). + assert order.index("_merge_cocoapods_components") < order.index( + "_apply_scope_filter" + ) def test_ingest_pipeline_never_references_the_scope_filter() -> None: diff --git a/docs-site/docs/user-guide/scans.md b/docs-site/docs/user-guide/scans.md index 11d02e08..96cd499a 100644 --- a/docs-site/docs/user-guide/scans.md +++ b/docs-site/docs/user-guide/scans.md @@ -155,6 +155,27 @@ Earlier builds ran the OSS Review Toolkit (ORT) at the license stage. v0.10.0 re If the local Trivy DB has not finished downloading when stage 5 runs (most common on a fresh install), the scan completes with **0 vulnerability findings** and a banner on the Vulnerabilities tab pointing operators at [Vulnerability data (Trivy DB)](../admin-guide/vulnerability-data.md). The automatic re-match beat picks up findings once the DB lands — no re-scan needed. +### iOS projects (CocoaPods / Swift Package Manager) {#ios-projects} + +iOS repositories scan offline from their **committed lockfiles** — the worker +needs neither Xcode, a Swift toolchain, nor the CocoaPods CLI: + +- **CocoaPods** — `cdxgen` cannot catalog pods without the `pod` CLI, so the + scanner excludes that cataloger (avoiding a crash that used to kill the + whole scan) and reconstructs the pod set and its dependency graph directly + from `Podfile.lock`. The `PODS:` block is the fully resolved truth: direct + and transitive pods, pinned versions, and each pod's sub-dependencies — + subspecs like `Moya/Core` appear as their own components. Pods carry no + Usage (Required/Optional) value because `Podfile.lock` records no + runtime/test distinction; the column shows `—`. +- **Swift Package Manager** — a committed `Package.resolved` is parsed by + `cdxgen` as-is (both the v1 and v2 formats). The sidecar executors skip + `swift package resolve` when the file is committed — the committed lockfile + already is the resolved graph, and re-resolving would hit the network. + +Commit `Podfile.lock` and `Package.resolved` to the repository — without +them an iOS scan finds only what the manifests declare. + ## Average duration | Project size | Source scan | Container scan | diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md index dae3754c..862575f0 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/scans.md @@ -155,6 +155,26 @@ queued ─────► running ─────► succeeded 5단계 실행 시 로컬 Trivy DB가 아직 다운로드되지 않았다면(첫 설치에서 가장 흔함) 스캔은 **취약점 finding 0개**로 완료되며 Vulnerabilities 탭에 [취약점 데이터 (Trivy DB)](../admin-guide/vulnerability-data.md)를 가리키는 배너가 표시됩니다. 자동 재매칭 beat이 DB 도착 후 finding을 가져옵니다 — 재스캔 불필요. +### iOS 프로젝트 (CocoaPods / Swift Package Manager) {#ios-projects} + +iOS 저장소는 **커밋된 lockfile**만으로 오프라인 스캔됩니다 — 워커에 Xcode, +Swift 툴체인, CocoaPods CLI 가 필요하지 않습니다. + +- **CocoaPods** — `cdxgen` 은 `pod` CLI 없이는 pod 를 수집하지 못하므로, + 스캐너가 해당 수집기를 제외하고(예전에는 스캔 전체가 죽던 크래시를 회피) + `Podfile.lock` 에서 pod 목록과 의존성 그래프를 직접 복원합니다. `PODS:` + 블록이 완전히 해석된 진실입니다: 직접·전이 pod, 고정 버전, 각 pod 의 하위 + 의존성이 모두 담겨 있고, `Moya/Core` 같은 서브스펙도 별도 컴포넌트로 + 나타납니다. `Podfile.lock` 에는 런타임/테스트 구분이 없어 pod 의 + Usage(Required/Optional) 값은 `—` 로 표시됩니다. +- **Swift Package Manager** — 커밋된 `Package.resolved` 는 `cdxgen` 이 그대로 + 파싱합니다(v1·v2 형식 모두). 파일이 커밋돼 있으면 사이드카 실행기는 + `swift package resolve` 를 건너뜁니다 — 커밋된 lockfile 이 이미 해석된 + 그래프이고, 다시 해석하면 네트워크에 접근하게 됩니다. + +`Podfile.lock` 과 `Package.resolved` 를 저장소에 커밋하십시오 — 없으면 iOS +스캔은 manifest 가 선언한 것만 찾습니다. + ## 평균 소요 시간 | 프로젝트 크기 | 소스 스캔 | 컨테이너 스캔 | From 6e8c2fced3ee2215677c2d4b4b5d894e6595b941 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 00:55:46 +0900 Subject: [PATCH 04/10] feat(scan): flag end-of-life components from a vendored endoflife.date snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An EOL runtime or framework gets no upstream fixes — a later Critical has no patch. That is a supply-chain risk axis distinct from CVEs, and TRUSCA had no signal for it. Mirror BomLens #368 (enrich-eol.sh) as a catalog-layer enrichment: - EOL lives on component_versions (0038: six nullable columns + a partial index WHERE eol_state='eol') — the KEV shape: a fact about product+cycle shared across scans/projects, re-stampable when the snapshot moves. NULL = not a tracked product; the whitelist philosophy never guesses. - services/eol: the purl->product map vendored VERBATIM from BomLens (byte-identity contract test against a captured copy; the %40 npm-scope encoding divergence is absorbed in the matcher, keeping the map exact) + a compact endoflife.date snapshot vendored into the repo (16 KB, 10 products) so Docker builds stay network-free and air-gapped installs work out of the box. scripts/refresh_eol_snapshot.py (port of BomLens build-eol-index.py: 15s/request, 60s budget, failed products skipped) refreshes it per release; a map<->snapshot product-set contract test trips when the map changes without a refresh. - Evaluation is the BomLens semantics table-tested: cycle derivation (leading numeric segments, major vs major.minor), bool eol as-is, ISO date vs today, everything else unknown. express-session must not match pkg:npm/express@ (over-match guard test). - Stamping happens inside persist_sbom_components right after the catalog row upsert — one evaluator per persist call, changed-value-guarded (idempotent re-scan), covering the source-scan AND sbom-ingest paths for free. Never fatal: corrupt/missing dataset degrades to no stamping with one aggregate log line. - Config: EOL_ENABLED default ON (offline, zero egress — contrast SCANOSS), EOL_SNAPSHOT_PATH for air-gapped snapshot mounts. - Verified: alembic upgrade head applies 0038 on live PostgreSQL 17 (columns + partial index present); full unit suite green. API/FE surfacing and the refresh beat land in the next two changes. --- .../versions/0038_component_version_eol.py | 112 +++++++ apps/backend/core/config.py | 38 +++ apps/backend/models/scan.py | 25 ++ apps/backend/scripts/refresh_eol_snapshot.py | 121 +++++++ apps/backend/services/eol/__init__.py | 35 ++ apps/backend/services/eol/eol_catalog.py | 290 ++++++++++++++++ apps/backend/services/eol/eol_purl_map.json | 22 ++ apps/backend/services/eol/eol_snapshot.json | 1 + apps/backend/tasks/scan_source.py | 34 ++ .../fixtures/eol/bomlens-eol-purl-map.json | 22 ++ .../tests/fixtures/eol/eol-components.json | 20 ++ apps/backend/tests/fixtures/eol/eol-data.json | 15 + .../tests/unit/services/test_eol_catalog.py | 315 ++++++++++++++++++ .../services/test_eol_catalog_contracts.py | 74 ++++ .../unit/tasks/test_scan_source_eol_stamp.py | 169 ++++++++++ 15 files changed, 1293 insertions(+) create mode 100644 apps/backend/alembic/versions/0038_component_version_eol.py create mode 100644 apps/backend/scripts/refresh_eol_snapshot.py create mode 100644 apps/backend/services/eol/__init__.py create mode 100644 apps/backend/services/eol/eol_catalog.py create mode 100644 apps/backend/services/eol/eol_purl_map.json create mode 100644 apps/backend/services/eol/eol_snapshot.json create mode 100644 apps/backend/tests/fixtures/eol/bomlens-eol-purl-map.json create mode 100644 apps/backend/tests/fixtures/eol/eol-components.json create mode 100644 apps/backend/tests/fixtures/eol/eol-data.json create mode 100644 apps/backend/tests/unit/services/test_eol_catalog.py create mode 100644 apps/backend/tests/unit/services/test_eol_catalog_contracts.py create mode 100644 apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py diff --git a/apps/backend/alembic/versions/0038_component_version_eol.py b/apps/backend/alembic/versions/0038_component_version_eol.py new file mode 100644 index 00000000..ce023244 --- /dev/null +++ b/apps/backend/alembic/versions/0038_component_version_eol.py @@ -0,0 +1,112 @@ +"""component_versions end-of-life (EOL) columns + partial index — EOL DB layer + +Revision ID: 0038 +Revises: 0037 +Create Date: 2026-07-11 + +Phase: EOL (endoflife.date end-of-life flagging — DB layer, Phase M) +PR: feat/eol-catalog +Kind: schema (additive — expand step; no data migration) +Forward-only: yes + +What: + - Add six columns to the existing ``component_versions`` table:: + + eol_state VARCHAR(16) NULL -- 'eol' | 'supported' | 'unknown' + eol_product VARCHAR(64) NULL -- endoflife.date product slug + eol_cycle VARCHAR(32) NULL -- derived release cycle ('3.2') + eol_date DATE NULL -- published EOL date (when dated) + eol_source VARCHAR(64) NULL -- 'endoflife.date@YYYY-MM-DD' + eol_evaluated_at TIMESTAMPTZ NULL -- last stamp time + + - Add a PARTIAL index on the flagged rows:: + + CREATE INDEX ix_component_versions_eol + ON component_versions (eol_state) + WHERE eol_state = 'eol'; + +Why: + - End-of-life is a fact about *product + release cycle* — identical for + every scan and project observing the same ``purl_with_version``. That is + the KEV shape (0034: ``kev`` lives on the shared ``vulnerabilities`` + catalog, not per-scan ``vulnerability_findings``), so EOL lives on the + shared ``component_versions`` catalog, not on ``scan_components``. + Catalog storage keeps re-evaluation possible: a newer endoflife.date + snapshot re-stamps existing rows (tasks/eol_catalog_refresh, Phase M + PR-3) exactly like kev_catalog_refresh re-stamps ``vulnerabilities``. + - All columns NULLable — NULL means "never evaluated / not a tracked + product", mirroring the enrichment's closed-whitelist philosophy (an + unmapped component is left untouched, never guessed). ``eol_state`` is + VARCHAR, not a native ENUM (same reasoning as ``kev_sync_state``'s + ``last_result``: a closed vocabulary enforced at the application layer + keeps additive vocabulary changes migration-free). + +Why partial (not a plain b-tree on ``eol_state``): + - Tracked products are a curated ~10-rule whitelist of frameworks; EOL + rows are a tiny minority of the cross-project component catalog. The + ``?eol=true`` filter and the overview/health counts read only flagged + rows — ``WHERE eol_state = 'eol'`` indexes exactly that set (same + reasoning as 0034's ``WHERE kev``). + +Notes: + - **Expand step only** (CLAUDE.md §6). Nullable ADD COLUMNs are + metadata-only on PG 11+ (no rewrite); the partial index covers zero + rows at upgrade time. The data fill happens at persist time + (persist_sbom_components) and via the refresh task — kept out of this + schema revision per the schema/data separation policy. + - NOT created ``CONCURRENTLY`` — Alembic migrations run in a transaction + and the index covers zero qualifying rows at build time (0034 note). + - Forward-only per CLAUDE.md §6: ``downgrade()`` raises + ``NotImplementedError``. +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import sqlalchemy as sa + +from alembic import op + +revision: str = "0038" +down_revision: str | None = "0037" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "component_versions", + sa.Column("eol_state", sa.String(16), nullable=True), + ) + op.add_column( + "component_versions", + sa.Column("eol_product", sa.String(64), nullable=True), + ) + op.add_column( + "component_versions", + sa.Column("eol_cycle", sa.String(32), nullable=True), + ) + op.add_column( + "component_versions", + sa.Column("eol_date", sa.Date(), nullable=True), + ) + op.add_column( + "component_versions", + sa.Column("eol_source", sa.String(64), nullable=True), + ) + op.add_column( + "component_versions", + sa.Column("eol_evaluated_at", sa.DateTime(timezone=True), nullable=True), + ) + op.create_index( + "ix_component_versions_eol", + "component_versions", + ["eol_state"], + unique=False, + postgresql_where=sa.text("eol_state = 'eol'"), + ) + + +def downgrade() -> None: + raise NotImplementedError("forward-only migration (CLAUDE.md §6)") diff --git a/apps/backend/core/config.py b/apps/backend/core/config.py index 73373beb..f0ffd0e6 100644 --- a/apps/backend/core/config.py +++ b/apps/backend/core/config.py @@ -1340,6 +1340,44 @@ def kev_refresh_timeout_seconds() -> int: ) +# --------------------------------------------------------------------------- +# Phase M — end-of-life (EOL) component flagging (endoflife.date). +# +# The default path is FULLY offline: verdicts come from a snapshot vendored +# into the repo (services/eol/eol_snapshot.json, refreshed per release by +# scripts/refresh_eol_snapshot.py), so EOL_ENABLED defaults ON — reading a +# local file has zero egress (contrast SCANOSS). The optional live-refresh +# beat (PR M-3) is a SEPARATE, default-OFF toggle because that one does +# introduce new egress. Accessors resolve env at call time (rule #11). +# --------------------------------------------------------------------------- + + +def eol_enabled() -> bool: + """Whether components are stamped with endoflife.date EOL verdicts. + + Default ``true`` — offline, additive, never fatal (a missing/corrupt + dataset just skips stamping with one WARNING per scan). Only the exact + falsy tokens ``false`` / ``0`` / ``no`` disable. Read at call time + (rule #11). + """ + return os.getenv("EOL_ENABLED", "true").strip().lower() not in { + "false", + "0", + "no", + } + + +def eol_snapshot_path() -> str: + """Operator override for the endoflife.date snapshot file. + + Empty (default) → the vendored ``services/eol/eol_snapshot.json``. + Air-gapped installs can mount a fresher snapshot (built with + ``scripts/refresh_eol_snapshot.py`` on a connected host) and point this + at it. Read at call time (rule #11). + """ + return os.getenv("EOL_SNAPSHOT_PATH", "").strip() + + # --------------------------------------------------------------------------- # Phase 2 PR #9 — WebSocket gateway configuration accessors. # diff --git a/apps/backend/models/scan.py b/apps/backend/models/scan.py index 33511054..c9bffbb5 100644 --- a/apps/backend/models/scan.py +++ b/apps/backend/models/scan.py @@ -506,6 +506,24 @@ class ComponentVersion(Base): DateTime(timezone=True), nullable=False, server_default=NOW ) + # Phase M — end-of-life flag (endoflife.date, 0038). EOL is a fact about + # product + release cycle, shared across every scan/project observing + # this purl_with_version — the KEV shape (kev on the vulnerabilities + # catalog), so it lives here, not on ScanComponent. All NULLable: NULL = + # never evaluated / not a tracked product (closed whitelist — an + # unmapped component is never guessed). eol_state is a closed vocabulary + # ('eol' | 'supported' | 'unknown') enforced at the application layer + # (services/eol/eol_catalog.py), VARCHAR not native ENUM per the + # kev_sync_state.last_result convention. + eol_state: Mapped[str | None] = mapped_column(String(16), nullable=True) + eol_product: Mapped[str | None] = mapped_column(String(64), nullable=True) + eol_cycle: Mapped[str | None] = mapped_column(String(32), nullable=True) + eol_date: Mapped[date | None] = mapped_column(Date, nullable=True) + eol_source: Mapped[str | None] = mapped_column(String(64), nullable=True) + eol_evaluated_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + component: Mapped[Component] = relationship(back_populates="versions") __table_args__ = ( @@ -516,6 +534,13 @@ class ComponentVersion(Base): # (e.g. com.github.jnr/jffi@1.3.1 vs same?classifier=native). See # alembic/versions/0027_drop_redundant_component_version_unique.py. Index("ix_component_versions_component_id", "component_id"), + # Partial: EOL rows are a tiny curated minority — the ?eol=true filter + # and the overview/health counts read only flagged rows (0038). + Index( + "ix_component_versions_eol", + "eol_state", + postgresql_where=text("eol_state = 'eol'"), + ), ) diff --git a/apps/backend/scripts/refresh_eol_snapshot.py b/apps/backend/scripts/refresh_eol_snapshot.py new file mode 100644 index 00000000..fe8962dc --- /dev/null +++ b/apps/backend/scripts/refresh_eol_snapshot.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +refresh_eol_snapshot.py — rebuild the vendored endoflife.date snapshot. + +Maintainer-run, once per release (and whenever ``services/eol/eol_purl_map.json`` +gains a product):: + + python3 scripts/refresh_eol_snapshot.py + +Port of BomLens ``docker/build-eol-index.py`` with one structural change: +BomLens bakes the snapshot at Docker BUILD time, TRUSCA vendors it into the +repo — Docker builds stay network-free/reproducible and air-gapped installs +get a working dataset out of the box. Scan-time is offline either way. + +Fetches https://endoflife.date/api/{product}.json for exactly the products +the map references, keeps only the fields the evaluator reads, and writes a +compact JSON (a few KB) with the build date under ``_snapshot``. + +Attribution: end-of-life dates are sourced from https://endoflife.date +(code MIT; the lifecycle dates are factual data). The snapshot date is +surfaced per stamped row via ``component_versions.eol_source``. + +Best-effort: a product whose fetch fails is skipped with a warning; if +NOTHING is fetched the existing vendored file is left untouched (EOL +flagging keeps using the previous snapshot). +""" + +from __future__ import annotations + +import datetime +import json +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path + +API = "https://endoflife.date/api/{}.json" +# Only the fields the evaluator (and the M-3 staleness surface) reads. +KEEP = ("cycle", "eol", "releaseDate", "latest", "latestReleaseDate") +# Bound the whole run so a black-holed network cannot stall a release build: +# per-request cap + total budget; products past the deadline are skipped. +REQUEST_TIMEOUT = 15 +TOTAL_BUDGET = 60 + +EOL_DIR = Path(__file__).resolve().parent.parent / "services" / "eol" +MAP_PATH = EOL_DIR / "eol_purl_map.json" +OUT_PATH = EOL_DIR / "eol_snapshot.json" + + +def distinct_products(map_path: Path) -> list[str]: + data = json.loads(map_path.read_text(encoding="utf-8")) + seen: list[str] = [] + for rule in data.get("rules", []): + product = rule.get("product") + if product and product not in seen: + seen.append(product) + return seen + + +def fetch(product: str) -> list[dict[str, object]]: + # S310: scheme is fixed https on a constant host (API template above) — + # product slugs come from the vendored map, not user input. + url = API.format(product) + request = urllib.request.Request( # noqa: S310 + url, headers={"Accept": "application/json"} + ) + with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as response: # noqa: S310 + cycles = json.load(response) + return [{k: c[k] for k in KEEP if k in c} for c in cycles] + + +def main() -> int: + products = distinct_products(MAP_PATH) + out: dict[str, object] = {"_snapshot": datetime.date.today().isoformat()} + ok = 0 + failed: list[str] = [] + skipped: list[str] = [] + deadline = time.monotonic() + TOTAL_BUDGET + for product in products: + if time.monotonic() > deadline: + skipped.append(product) + continue + try: + out[product] = fetch(product) + ok += 1 + except (urllib.error.URLError, OSError, ValueError, KeyError) as exc: + failed.append(product) + sys.stderr.write(f"[eol-snapshot] WARN: could not fetch {product}: {exc}\n") + if skipped: + sys.stderr.write( + f"[eol-snapshot] WARN: time budget ({TOTAL_BUDGET}s) spent; " + f"skipped {len(skipped)}: {skipped}\n" + ) + + if ok == 0: + sys.stderr.write( + "[eol-snapshot] WARN: fetched 0 products; keeping the existing " + "vendored snapshot untouched.\n" + ) + return 0 + if failed: + sys.stderr.write( + f"[eol-snapshot] WARN: {len(failed)} product(s) failed and will be " + f"missing from the snapshot: {failed} — components mapping to them " + "will evaluate to 'unknown'. Re-run to fill them in.\n" + ) + + OUT_PATH.write_text( + json.dumps(out, separators=(",", ":"), sort_keys=True) + "\n", + encoding="utf-8", + ) + sys.stderr.write( + f"[eol-snapshot] bundled {ok} product(s) into {OUT_PATH} " + f"(snapshot {out['_snapshot']}); {len(failed)} failed\n" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/apps/backend/services/eol/__init__.py b/apps/backend/services/eol/__init__.py new file mode 100644 index 00000000..027f2e84 --- /dev/null +++ b/apps/backend/services/eol/__init__.py @@ -0,0 +1,35 @@ +"""End-of-life (EOL) component flagging — Phase M. + +Package layout: + - ``eol_catalog.py`` — pure evaluation logic (rules → cycle → verdict). + - ``eol_purl_map.json`` — the purl→product whitelist, vendored VERBATIM + from BomLens ``docker/lib/eol-purl-map.json`` (consistency-contract + tested against a captured copy, the G7 registry precedent). + - ``eol_snapshot.json`` — compact endoflife.date dataset, vendored into + the repo and refreshed per release via + ``scripts/refresh_eol_snapshot.py`` (zero network at scan time). +""" + +from services.eol.eol_catalog import ( + EolDataset, + EolRule, + EolVerdict, + build_evaluator, + derive_cycle, + evaluate, + load_dataset, + load_rules, + stamp_component_version, +) + +__all__ = [ + "EolDataset", + "EolRule", + "EolVerdict", + "build_evaluator", + "derive_cycle", + "evaluate", + "load_dataset", + "load_rules", + "stamp_component_version", +] diff --git a/apps/backend/services/eol/eol_catalog.py b/apps/backend/services/eol/eol_catalog.py new file mode 100644 index 00000000..795f196e --- /dev/null +++ b/apps/backend/services/eol/eol_catalog.py @@ -0,0 +1,290 @@ +""" +EOL evaluation — endoflife.date verdicts for catalog components (Phase M). + +Port of BomLens ``docker/lib/enrich-eol.sh`` semantics (jq → Python): + + 1. Match a component purl against the vendored whitelist + (``eol_purl_map.json``): the FIRST rule whose ``purlPrefix`` is a + prefix of the purl wins. **Unmapped components return ``None`` — + never a guess** (closed-whitelist philosophy; endoflife.date covers + runtimes/frameworks, long-tail libraries are intentionally absent). + 2. Derive the release cycle from the version per the rule's granularity + (``major`` → first numeric segment; ``major.minor`` → first two). + 3. Look the cycle up in the snapshot dataset. ``entry.eol`` may be a + boolean (verdict as-is) or an ISO date string (EOL iff before today); + anything else — including a missing cycle — is ``unknown``. + +One deliberate divergence from the BomLens jq: purls are normalised +``%40 → @`` before prefix matching. Spec-compliant cdxgen emits scoped npm +packages URL-encoded (``pkg:npm/%40angular/core@…``) while the vendored map +spells ``pkg:npm/@angular/core@`` — absorbing the encoding in the matcher +keeps the map byte-identical to BomLens (the consistency contract stays +exact). + +Failure posture: evaluation is pure and total (no I/O after load); the +loaders return ``None``/empty on any problem so the persist hook degrades to +"no enrichment" — an EOL failure must never break a scan. +""" + +from __future__ import annotations + +import json +import re +from dataclasses import dataclass +from datetime import UTC, date, datetime +from functools import lru_cache +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +import structlog + +from core.config import eol_snapshot_path + +if TYPE_CHECKING: + from models import ComponentVersion + +log = structlog.get_logger("services.eol") + +_MAP_PATH = Path(__file__).resolve().parent / "eol_purl_map.json" +_SNAPSHOT_PATH = Path(__file__).resolve().parent / "eol_snapshot.json" + +_LEADING_INT = re.compile(r"^([0-9]+)") + +EolState = Literal["eol", "supported", "unknown"] + +# Closed vocabulary persisted into component_versions.eol_state (0038). +EOL_STATES: tuple[EolState, ...] = ("eol", "supported", "unknown") + + +@dataclass(frozen=True) +class EolRule: + purl_prefix: str + product: str + cycle: str # "major" | "major.minor" + + +@dataclass(frozen=True) +class EolVerdict: + state: EolState + product: str + cycle: str | None + date: date | None + source: str + + +@dataclass(frozen=True) +class EolDataset: + """Compact endoflife.date snapshot: product slug → cycle entries.""" + + snapshot: str # ISO date the snapshot was built + products: dict[str, list[dict[str, Any]]] + + def cycles(self, product: str) -> list[dict[str, Any]]: + entries = self.products.get(product) + return entries if isinstance(entries, list) else [] + + +# --------------------------------------------------------------------------- +# Loading (vendored package data — static files, lru_cache is correct here; +# CLAUDE.md rule #11 concerns env config, and the ONLY env-driven part — +# EOL_SNAPSHOT_PATH — is resolved per call in load_dataset). +# --------------------------------------------------------------------------- + + +@lru_cache(maxsize=1) +def load_rules() -> tuple[EolRule, ...]: + """Parse and cache the vendored purl→product whitelist.""" + try: + raw = json.loads(_MAP_PATH.read_text(encoding="utf-8")) + rules = [] + for entry in raw.get("rules", []): + if not isinstance(entry, dict): + continue + prefix = entry.get("purlPrefix") + product = entry.get("product") + cycle = entry.get("cycle") + if ( + isinstance(prefix, str) + and prefix + and isinstance(product, str) + and product + and cycle in ("major", "major.minor") + ): + rules.append( + EolRule(purl_prefix=prefix, product=product, cycle=cycle) + ) + return tuple(rules) + except (OSError, ValueError) as exc: # pragma: no cover — vendored file + log.warning("eol_rules_load_failed", error=str(exc)) + return () + + +def load_dataset() -> EolDataset | None: + """Load the effective snapshot dataset; ``None`` disables enrichment. + + Resolution order: operator override (``EOL_SNAPSHOT_PATH``, read at call + time — air-gapped installs mount a fresher file) → the vendored package + file. PR M-3 extends this with the ``eol_sync_state`` DB snapshot, + preferring the newer of the two. + """ + override = eol_snapshot_path() + path = Path(override) if override else _SNAPSHOT_PATH + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + log.warning("eol_dataset_load_failed", path=str(path), error=str(exc)) + return None + if not isinstance(raw, dict): + return None + snapshot = raw.get("_snapshot") + if not isinstance(snapshot, str) or not snapshot: + return None + products = { + key: value + for key, value in raw.items() + if not key.startswith("_") and isinstance(value, list) + } + if not products: + return None + return EolDataset(snapshot=snapshot, products=products) + + +# --------------------------------------------------------------------------- +# Evaluation +# --------------------------------------------------------------------------- + + +def derive_cycle(version: str, granularity: str) -> str | None: + """Release cycle from a version string (BomLens ``enrich-eol.sh:77-85``). + + Strip a leading ``v``, split on ``.``, keep the leading numeric part of + each segment and stop at the first segment without one. ``major`` → + first segment; ``major.minor`` → first two (falls back to the first when + only one exists). Fully non-numeric → ``None`` (unknown). + + Note: the BomLens jq errors out (→ unknown) on mid-string non-numeric + segments; this port implements the documented "stop at the first + non-numeric-lead segment" semantics — table-tested. + """ + segments: list[str] = [] + for part in version.lstrip("v").split("."): + match = _LEADING_INT.match(part) + if not match: + break + segments.append(match.group(1)) + if not segments: + return None + if granularity == "major": + return segments[0] + if len(segments) >= 2: + return f"{segments[0]}.{segments[1]}" + return segments[0] + + +def evaluate( + purl_with_version: str, + version: str, + *, + rules: tuple[EolRule, ...], + dataset: EolDataset, + today: date, +) -> EolVerdict | None: + """Verdict for one component; ``None`` = unmapped (leave columns NULL).""" + purl = purl_with_version.replace("%40", "@") + rule = next((r for r in rules if purl.startswith(r.purl_prefix)), None) + if rule is None: + return None + source = f"endoflife.date@{dataset.snapshot}" + cycle = derive_cycle(version, rule.cycle) + if cycle is None: + return EolVerdict("unknown", rule.product, None, None, source) + entry = next( + ( + e + for e in dataset.cycles(rule.product) + if isinstance(e, dict) and str(e.get("cycle")) == cycle + ), + None, + ) + if entry is None: + return EolVerdict("unknown", rule.product, cycle, None, source) + eol_value = entry.get("eol") + if isinstance(eol_value, bool): + state: EolState = "eol" if eol_value else "supported" + return EolVerdict(state, rule.product, cycle, None, source) + if isinstance(eol_value, str): + try: + eol_date = date.fromisoformat(eol_value) + except ValueError: + return EolVerdict("unknown", rule.product, cycle, None, source) + state = "eol" if eol_date < today else "supported" + return EolVerdict(state, rule.product, cycle, eol_date, source) + return EolVerdict("unknown", rule.product, cycle, None, source) + + +# --------------------------------------------------------------------------- +# Persistence glue +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class _Evaluator: + """Rules + dataset bound once per persist call (O(rules) per component).""" + + rules: tuple[EolRule, ...] + dataset: EolDataset + today: date + + def verdict_for(self, purl_with_version: str, version: str) -> EolVerdict | None: + return evaluate( + purl_with_version, + version, + rules=self.rules, + dataset=self.dataset, + today=self.today, + ) + + +def build_evaluator() -> _Evaluator | None: + """One evaluator per persist call; ``None`` disables stamping quietly.""" + rules = load_rules() + if not rules: + return None + dataset = load_dataset() + if dataset is None: + return None + return _Evaluator( + rules=rules, dataset=dataset, today=datetime.now(UTC).date() + ) + + +def stamp_component_version( + component_version: ComponentVersion, + verdict: EolVerdict | None, + now: datetime, +) -> bool: + """Write a verdict onto the catalog row; changed-value-guarded. + + Returns ``True`` when any column actually changed (the KEV + ``_apply_listing`` idiom — an unchanged row is not dirtied, so a + re-scan or the weekly re-stamp pass stays idempotent). ``verdict=None`` + (unmapped) leaves the row untouched: clearing stale stamps after a map + change is the refresh task's job (PR M-3), not the per-scan hook's. + """ + if verdict is None: + return False + changed = False + updates = { + "eol_state": verdict.state, + "eol_product": verdict.product, + "eol_cycle": verdict.cycle, + "eol_date": verdict.date, + "eol_source": verdict.source, + } + for attr, value in updates.items(): + if getattr(component_version, attr) != value: + setattr(component_version, attr, value) + changed = True + if changed: + component_version.eol_evaluated_at = now + return changed diff --git a/apps/backend/services/eol/eol_purl_map.json b/apps/backend/services/eol/eol_purl_map.json new file mode 100644 index 00000000..b28ff4ce --- /dev/null +++ b/apps/backend/services/eol/eol_purl_map.json @@ -0,0 +1,22 @@ +{ + "_comment": "Component PURL coordinate -> endoflife.date product, for offline end-of-life (EOL) flagging (enrich-eol.sh). A component whose purl starts with a rule's `purlPrefix` is mapped to that endoflife product; its version's leading segments form the release `cycle` (per the rule's `cycle` granularity), which is looked up in the bundled endoflife dataset (eol-data.json) to decide EOL. This is a CLOSED, curated whitelist on purpose: an unmapped component gets bomlens:eol=unknown, never a guess. Match is by PURL, not display name, because one product arrives under many component names (e.g. spring-boot-starter-web, spring-boot-autoconfigure all map to spring-boot). endoflife.date coverage is runtimes/frameworks/OS, so long-tail libraries are intentionally absent. Keep this map and eol-data.json's product set in sync.", + + "_cycle": "cycle granularity: 'major' uses the first numeric version segment (express 4.18.2 -> '4'); 'major.minor' uses the first two joined by '.' (spring-boot 3.2.0 -> '3.2'). Must match how endoflife.date keys that product's cycles.", + + "rules": [ + { "purlPrefix": "pkg:maven/org.springframework.boot/", "product": "spring-boot", "cycle": "major.minor" }, + { "purlPrefix": "pkg:maven/org.springframework/", "product": "spring-framework", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:npm/express@", "product": "express", "cycle": "major" }, + { "purlPrefix": "pkg:npm/next@", "product": "nextjs", "cycle": "major.minor" }, + { "purlPrefix": "pkg:npm/@angular/core@", "product": "angular", "cycle": "major" }, + { "purlPrefix": "pkg:npm/vue@", "product": "vue", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:pypi/django@", "product": "django", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:gem/rails@", "product": "rails", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:composer/symfony/symfony@", "product": "symfony", "cycle": "major.minor" }, + { "purlPrefix": "pkg:composer/laravel/framework@", "product": "laravel", "cycle": "major" } + ] +} diff --git a/apps/backend/services/eol/eol_snapshot.json b/apps/backend/services/eol/eol_snapshot.json new file mode 100644 index 00000000..3f539b9c --- /dev/null +++ b/apps/backend/services/eol/eol_snapshot.json @@ -0,0 +1 @@ +{"_snapshot":"2026-07-11","angular":[{"cycle":"22","eol":false,"latest":"22.0.6","latestReleaseDate":"2026-07-08","releaseDate":"2026-06-03"},{"cycle":"21","eol":"2027-05-19","latest":"21.2.18","latestReleaseDate":"2026-07-08","releaseDate":"2025-11-19"},{"cycle":"20","eol":"2026-11-28","latest":"20.3.26","latestReleaseDate":"2026-07-08","releaseDate":"2025-05-28"},{"cycle":"19","eol":"2026-05-19","latest":"19.2.25","latestReleaseDate":"2026-06-02","releaseDate":"2024-11-19"},{"cycle":"18","eol":"2025-11-21","latest":"18.2.14","latestReleaseDate":"2025-09-10","releaseDate":"2024-05-22"},{"cycle":"17","eol":"2025-05-15","latest":"17.3.12","latestReleaseDate":"2024-07-17","releaseDate":"2023-11-08"},{"cycle":"16","eol":"2024-11-08","latest":"16.2.12","latestReleaseDate":"2023-11-02","releaseDate":"2023-05-03"},{"cycle":"15","eol":"2024-05-18","latest":"15.2.10","latestReleaseDate":"2023-10-04","releaseDate":"2022-11-16"},{"cycle":"14","eol":"2023-11-18","latest":"14.3.0","latestReleaseDate":"2023-03-13","releaseDate":"2022-06-02"},{"cycle":"13","eol":"2023-05-04","latest":"13.4.0","latestReleaseDate":"2023-04-06","releaseDate":"2021-11-03"},{"cycle":"12","eol":"2022-11-12","latest":"12.2.17","latestReleaseDate":"2022-11-22","releaseDate":"2021-05-13"},{"cycle":"11","eol":"2022-05-11","latest":"11.2.14","latestReleaseDate":"2021-05-12","releaseDate":"2020-11-11"},{"cycle":"10","eol":"2021-12-24","latest":"10.2.5","latestReleaseDate":"2021-04-21","releaseDate":"2020-06-24"},{"cycle":"9","eol":"2021-08-06","latest":"9.1.13","latestReleaseDate":"2020-12-16","releaseDate":"2020-02-06"}],"django":[{"cycle":"6.0","eol":"2027-04-30","latest":"6.0.7","latestReleaseDate":"2026-07-07","releaseDate":"2025-12-03"},{"cycle":"5.2","eol":"2028-04-30","latest":"5.2.16","latestReleaseDate":"2026-07-07","releaseDate":"2025-04-02"},{"cycle":"5.1","eol":"2025-12-03","latest":"5.1.15","latestReleaseDate":"2025-12-02","releaseDate":"2024-08-07"},{"cycle":"5.0","eol":"2025-04-02","latest":"5.0.14","latestReleaseDate":"2025-04-02","releaseDate":"2023-12-04"},{"cycle":"4.2","eol":"2026-04-07","latest":"4.2.30","latestReleaseDate":"2026-04-07","releaseDate":"2023-04-03"},{"cycle":"4.1","eol":"2023-12-01","latest":"4.1.13","latestReleaseDate":"2023-11-01","releaseDate":"2022-08-03"},{"cycle":"4.0","eol":"2023-04-01","latest":"4.0.10","latestReleaseDate":"2023-02-14","releaseDate":"2021-12-07"},{"cycle":"3.2","eol":"2024-04-01","latest":"3.2.25","latestReleaseDate":"2024-03-04","releaseDate":"2021-04-06"},{"cycle":"3.1","eol":"2021-12-07","latest":"3.1.14","latestReleaseDate":"2021-12-07","releaseDate":"2020-08-04"},{"cycle":"3.0","eol":"2021-04-06","latest":"3.0.14","latestReleaseDate":"2021-04-06","releaseDate":"2019-12-02"},{"cycle":"2.2","eol":"2022-04-11","latest":"2.2.28","latestReleaseDate":"2022-04-11","releaseDate":"2019-04-01"},{"cycle":"2.1","eol":"2019-12-02","latest":"2.1.15","latestReleaseDate":"2019-12-02","releaseDate":"2018-08-01"},{"cycle":"2.0","eol":"2019-04-01","latest":"2.0.13","latestReleaseDate":"2019-02-12","releaseDate":"2017-12-02"},{"cycle":"1.11","eol":"2020-04-01","latest":"1.11.29","latestReleaseDate":"2020-03-04","releaseDate":"2017-04-04"},{"cycle":"1.10","eol":"2017-12-02","latest":"1.10.8","latestReleaseDate":"2017-09-05","releaseDate":"2016-08-01"},{"cycle":"1.9","eol":"2017-04-04","latest":"1.9.13","latestReleaseDate":"2017-04-04","releaseDate":"2015-12-01"},{"cycle":"1.8","eol":"2018-04-01","latest":"1.8.19","latestReleaseDate":"2018-03-06","releaseDate":"2015-04-01"},{"cycle":"1.7","eol":"2015-12-01","latest":"1.7.11","latestReleaseDate":"2015-11-24","releaseDate":"2014-09-03"},{"cycle":"1.6","eol":"2015-04-01","latest":"1.6.11","latestReleaseDate":"2015-03-18","releaseDate":"2013-11-06"},{"cycle":"1.5","eol":"2014-09-02","latest":"1.5.12","latestReleaseDate":"2015-01-02","releaseDate":"2013-02-26"},{"cycle":"1.4","eol":"2015-10-01","latest":"1.4.22","latestReleaseDate":"2015-08-18","releaseDate":"2012-03-23"},{"cycle":"1.3","eol":"2013-02-26","latest":"1.3.7","latestReleaseDate":"2013-02-21","releaseDate":"2011-03-23"}],"express":[{"cycle":"5","eol":false,"latest":"5.2.1","latestReleaseDate":"2025-12-01","releaseDate":"2024-09-09"},{"cycle":"4","eol":false,"latest":"4.22.2","latestReleaseDate":"2026-05-11","releaseDate":"2014-04-09"},{"cycle":"3","eol":"2015-07-05","latest":"3.21.2","latestReleaseDate":"2015-07-31","releaseDate":"2012-10-23"},{"cycle":"2","eol":"2012-07-01","latest":"2.5.11","latestReleaseDate":"2012-06-29","releaseDate":"2011-03-17"},{"cycle":"1","eol":"2011-03-01","latest":"1.0.9","latestReleaseDate":"2011-03-23","releaseDate":"2010-11-16"}],"laravel":[{"cycle":"13","eol":"2028-03-17","latest":"13.19.0","latestReleaseDate":"2026-07-07","releaseDate":"2026-03-17"},{"cycle":"12","eol":"2027-02-24","latest":"12.63.0","latestReleaseDate":"2026-07-07","releaseDate":"2025-02-24"},{"cycle":"11","eol":"2026-03-12","latest":"11.54.0","latestReleaseDate":"2026-05-27","releaseDate":"2024-03-12"},{"cycle":"10","eol":"2025-02-04","latest":"10.50.2","latestReleaseDate":"2026-02-15","releaseDate":"2023-02-14"},{"cycle":"9","eol":"2024-02-06","latest":"9.52.21","latestReleaseDate":"2025-09-30","releaseDate":"2022-02-08"},{"cycle":"8","eol":"2023-01-24","latest":"8.83.29","latestReleaseDate":"2024-11-20","releaseDate":"2020-09-08"},{"cycle":"7","eol":"2021-03-03","latest":"7.30.7","latestReleaseDate":"2024-11-12","releaseDate":"2020-03-03"},{"cycle":"6","eol":"2022-09-06","latest":"6.20.45","latestReleaseDate":"2024-11-12","releaseDate":"2019-09-03"},{"cycle":"5.8","eol":"2020-02-26","latest":"5.8.38","latestReleaseDate":"2020-04-14","releaseDate":"2019-02-26"},{"cycle":"5.5","eol":"2020-08-30","latest":"5.5.50","latestReleaseDate":"2020-08-18","releaseDate":"2017-08-30"}],"nextjs":[{"cycle":"16","eol":false,"latest":"16.2.10","latestReleaseDate":"2026-07-01","releaseDate":"2025-10-22"},{"cycle":"15","eol":"2026-10-21","latest":"15.5.20","latestReleaseDate":"2026-07-01","releaseDate":"2024-10-21"},{"cycle":"14","eol":"2025-10-26","latest":"14.2.35","latestReleaseDate":"2025-12-11","releaseDate":"2023-10-26"},{"cycle":"13","eol":"2024-12-21","latest":"13.5.11","latestReleaseDate":"2025-03-27","releaseDate":"2022-10-25"},{"cycle":"12","eol":"2022-11-21","latest":"12.3.7","latestReleaseDate":"2025-03-28","releaseDate":"2021-10-26"},{"cycle":"11","eol":"2022-01-27","latest":"11.1.4","latestReleaseDate":"2022-01-27","releaseDate":"2021-06-15"},{"cycle":"10","eol":"2021-06-15","latest":"10.2.3","latestReleaseDate":"2021-05-24","releaseDate":"2020-10-27"},{"cycle":"9","eol":"2020-10-27","latest":"9.5.5","latestReleaseDate":"2020-10-10","releaseDate":"2019-07-08"}],"rails":[{"cycle":"8.1","eol":"2027-10-10","latest":"8.1.3","latestReleaseDate":"2026-03-24","releaseDate":"2025-10-22"},{"cycle":"8.0","eol":"2026-11-07","latest":"8.0.5","latestReleaseDate":"2026-03-24","releaseDate":"2024-11-07"},{"cycle":"7.2","eol":"2026-08-09","latest":"7.2.3.1","latestReleaseDate":"2026-03-23","releaseDate":"2024-08-09"},{"cycle":"7.1","eol":"2025-10-01","latest":"7.1.6","latestReleaseDate":"2025-10-28","releaseDate":"2023-10-05"},{"cycle":"7.0","eol":"2025-04-01","latest":"7.0.10","latestReleaseDate":"2025-10-28","releaseDate":"2021-12-15"},{"cycle":"6.1","eol":"2024-10-01","latest":"6.1.7.10","latestReleaseDate":"2024-10-23","releaseDate":"2020-12-09"},{"cycle":"6.0","eol":"2023-06-01","latest":"6.0.6.1","latestReleaseDate":"2023-01-17","releaseDate":"2019-08-16"},{"cycle":"5.2","eol":"2022-06-01","latest":"5.2.8.1","latestReleaseDate":"2022-07-12","releaseDate":"2018-04-09"},{"cycle":"5.1","eol":"2019-08-25","latest":"5.1.7","latestReleaseDate":"2019-03-27","releaseDate":"2017-04-27"},{"cycle":"5.0","eol":"2018-04-09","latest":"5.0.7.2","latestReleaseDate":"2019-03-13","releaseDate":"2016-06-30"},{"cycle":"4.2","eol":"2017-04-27","latest":"4.2.11.3","latestReleaseDate":"2020-05-15","releaseDate":"2014-12-19"}],"spring-boot":[{"cycle":"4.1","eol":"2027-07-31","latest":"4.1.0","latestReleaseDate":"2026-06-10","releaseDate":"2026-06-30"},{"cycle":"4.0","eol":"2026-12-31","latest":"4.0.7","latestReleaseDate":"2026-06-10","releaseDate":"2025-11-30"},{"cycle":"3.5","eol":"2026-06-30","latest":"3.5.16","latestReleaseDate":"2026-06-25","releaseDate":"2025-05-31"},{"cycle":"3.4","eol":"2025-12-31","latest":"3.4.13","latestReleaseDate":"2025-12-18","releaseDate":"2024-11-30"},{"cycle":"3.3","eol":"2025-06-30","latest":"3.3.13","latestReleaseDate":"2025-06-19","releaseDate":"2024-05-31"},{"cycle":"3.2","eol":"2024-12-31","latest":"3.2.12","latestReleaseDate":"2024-11-21","releaseDate":"2023-11-30"},{"cycle":"3.1","eol":"2024-06-30","latest":"3.1.12","latestReleaseDate":"2024-05-23","releaseDate":"2023-05-31"},{"cycle":"3.0","eol":"2023-12-31","latest":"3.0.13","latestReleaseDate":"2023-11-23","releaseDate":"2022-11-24"},{"cycle":"2.7","eol":"2023-06-30","latest":"2.7.18","latestReleaseDate":"2023-11-23","releaseDate":"2022-05-31"},{"cycle":"2.6","eol":"2022-11-24","latest":"2.6.15","latestReleaseDate":"2023-05-18","releaseDate":"2021-11-17"},{"cycle":"2.5","eol":"2022-05-19","latest":"2.5.15","latestReleaseDate":"2023-05-18","releaseDate":"2021-05-20"},{"cycle":"2.4","eol":"2021-11-18","latest":"2.4.13","latestReleaseDate":"2021-11-18","releaseDate":"2020-11-12"},{"cycle":"2.3","eol":"2021-05-20","latest":"2.3.12","latestReleaseDate":"2021-06-10","releaseDate":"2020-05-15"},{"cycle":"2.2","eol":"2020-10-16","latest":"2.2.13","latestReleaseDate":"2021-01-14","releaseDate":"2019-10-16"},{"cycle":"2.1","eol":"2019-10-30","latest":"2.1.18","latestReleaseDate":"2020-10-29","releaseDate":"2018-10-30"},{"cycle":"2.0","eol":"2019-03-01","latest":"2.0.9","latestReleaseDate":"2019-04-03","releaseDate":"2018-03-01"},{"cycle":"1.5","eol":"2019-08-06","latest":"1.5.22","latestReleaseDate":"2019-08-06","releaseDate":"2017-01-30"}],"spring-framework":[{"cycle":"7.0","eol":"2027-06-30","latest":"7.0.8","latestReleaseDate":"2026-06-08","releaseDate":"2025-11-30"},{"cycle":"6.2","eol":"2026-06-30","latest":"6.2.19","latestReleaseDate":"2026-06-08","releaseDate":"2024-11-30"},{"cycle":"6.1","eol":"2025-06-30","latest":"6.1.21","latestReleaseDate":"2025-06-12","releaseDate":"2023-11-30"},{"cycle":"6.0","eol":"2024-06-30","latest":"6.0.23","latestReleaseDate":"2024-08-14","releaseDate":"2022-11-30"},{"cycle":"5.3","eol":"2024-08-31","latest":"5.3.39","latestReleaseDate":"2024-08-14","releaseDate":"2020-10-31"},{"cycle":"5.2","eol":"2021-12-31","latest":"5.2.25","latestReleaseDate":"2023-07-13","releaseDate":"2019-09-30"},{"cycle":"5.1","eol":"2020-12-31","latest":"5.1.20","latestReleaseDate":"2020-12-09","releaseDate":"2018-09-21"},{"cycle":"5.0","eol":"2020-12-31","latest":"5.0.20","latestReleaseDate":"2020-12-09","releaseDate":"2017-09-28"},{"cycle":"4.3","eol":"2020-12-31","latest":"4.3.30","latestReleaseDate":"2020-12-09","releaseDate":"2016-06-10"},{"cycle":"3.2","eol":"2016-12-31","latest":"3.2.18","latestReleaseDate":"2016-12-21","releaseDate":"2012-12-13"}],"symfony":[{"cycle":"8.1","eol":"2027-01-31","latest":"8.1.1","latestReleaseDate":"2026-06-27","releaseDate":"2026-05-29"},{"cycle":"8.0","eol":"2026-07-31","latest":"8.0.14","latestReleaseDate":"2026-06-27","releaseDate":"2025-11-27"},{"cycle":"7.4","eol":"2029-11-30","latest":"7.4.14","latestReleaseDate":"2026-06-27","releaseDate":"2025-11-27"},{"cycle":"7.3","eol":"2026-01-31","latest":"7.3.11","latestReleaseDate":"2026-01-28","releaseDate":"2025-05-29"},{"cycle":"7.2","eol":"2025-07-31","latest":"7.2.9","latestReleaseDate":"2025-07-31","releaseDate":"2024-11-29"},{"cycle":"7.1","eol":"2025-01-31","latest":"7.1.11","latestReleaseDate":"2025-01-29","releaseDate":"2024-05-31"},{"cycle":"7.0","eol":"2024-07-31","latest":"7.0.10","latestReleaseDate":"2024-07-26","releaseDate":"2023-11-29"},{"cycle":"6.4","eol":"2027-11-30","latest":"6.4.42","latestReleaseDate":"2026-06-27","releaseDate":"2023-11-29"},{"cycle":"6.3","eol":"2024-01-31","latest":"6.3.12","latestReleaseDate":"2024-01-30","releaseDate":"2023-05-30"},{"cycle":"6.2","eol":"2023-07-31","latest":"6.2.14","latestReleaseDate":"2023-07-31","releaseDate":"2022-11-30"},{"cycle":"6.1","eol":"2023-01-31","latest":"6.1.12","latestReleaseDate":"2023-02-01","releaseDate":"2022-05-27"},{"cycle":"6.0","eol":"2023-01-31","latest":"6.0.20","latestReleaseDate":"2023-02-01","releaseDate":"2021-11-29"},{"cycle":"5.4","eol":"2029-02-28","latest":"5.4.53","latestReleaseDate":"2026-05-27","releaseDate":"2021-11-29"},{"cycle":"5.3","eol":"2022-01-01","latest":"5.3.16","latestReleaseDate":"2022-03-01","releaseDate":"2021-05-31"},{"cycle":"5.2","eol":"2021-07-21","latest":"5.2.14","latestReleaseDate":"2021-07-29","releaseDate":"2020-11-30"},{"cycle":"5.1","eol":"2021-01-21","latest":"5.1.11","latestReleaseDate":"2021-01-27","releaseDate":"2020-05-31"},{"cycle":"5.0","eol":"2020-07-21","latest":"5.0.11","latestReleaseDate":"2020-07-24","releaseDate":"2019-11-21"},{"cycle":"4.4","eol":"2023-11-21","latest":"4.4.51","latestReleaseDate":"2023-11-10","releaseDate":"2019-11-21"},{"cycle":"4.3","eol":"2020-07-01","latest":"4.3.11","latestReleaseDate":"2020-01-31","releaseDate":"2019-05-30"},{"cycle":"4.2","eol":"2020-01-01","latest":"4.2.12","latestReleaseDate":"2019-11-13","releaseDate":"2018-11-30"},{"cycle":"4.1","eol":"2019-07-01","latest":"4.1.13","latestReleaseDate":"2019-04-17","releaseDate":"2018-05-30"},{"cycle":"4.0","eol":"2019-01-01","latest":"4.0.15","latestReleaseDate":"2018-12-06","releaseDate":"2017-11-30"},{"cycle":"3.4","eol":"2021-11-01","latest":"3.4.49","latestReleaseDate":"2021-05-19","releaseDate":"2017-11-30"},{"cycle":"3.3","eol":"2018-07-01","latest":"3.3.18","latestReleaseDate":"2018-08-01","releaseDate":"2017-05-29"},{"cycle":"3.2","eol":"2018-01-01","latest":"3.2.14","latestReleaseDate":"2017-11-16","releaseDate":"2016-11-30"},{"cycle":"3.1","eol":"2017-07-01","latest":"3.1.10","latestReleaseDate":"2017-01-27","releaseDate":"2016-05-30"},{"cycle":"3.0","eol":"2017-01-01","latest":"3.0.9","latestReleaseDate":"2016-07-30","releaseDate":"2015-11-30"},{"cycle":"2.8","eol":"2019-11-01","latest":"2.8.52","latestReleaseDate":"2019-11-13","releaseDate":"2015-11-30"},{"cycle":"2.7","eol":"2019-05-01","latest":"2.7.52","latestReleaseDate":"2019-04-17","releaseDate":"2015-05-30"},{"cycle":"2.3","eol":"2017-05-01","latest":"2.3.42","latestReleaseDate":"2016-05-30","releaseDate":"2013-06-03"}],"vue":[{"cycle":"3.5","eol":false,"latest":"3.5.39","latestReleaseDate":"2026-06-25","releaseDate":"2024-09-03"},{"cycle":"3.4","eol":"2024-09-03","latest":"3.4.38","latestReleaseDate":"2024-08-15","releaseDate":"2023-12-29"},{"cycle":"3.3","eol":"2023-12-29","latest":"3.3.13","latestReleaseDate":"2023-12-19","releaseDate":"2023-05-11"},{"cycle":"2.7","eol":"2023-12-31","latest":"2.7.16","latestReleaseDate":"2023-12-24","releaseDate":"2022-07-01"},{"cycle":"3.2","eol":"2023-05-11","latest":"3.2.47","latestReleaseDate":"2023-02-02","releaseDate":"2021-08-09"},{"cycle":"3.1","eol":"2021-08-09","latest":"3.1.5","latestReleaseDate":"2021-07-16","releaseDate":"2021-06-07"},{"cycle":"3.0","eol":"2021-06-07","latest":"3.0.11","latestReleaseDate":"2021-04-01","releaseDate":"2020-09-18"},{"cycle":"2.6","eol":"2022-07-01","latest":"2.6.14","latestReleaseDate":"2021-06-07","releaseDate":"2019-02-04"},{"cycle":"2.5","eol":"2019-02-04","latest":"2.5.22","latestReleaseDate":"2019-01-11","releaseDate":"2017-10-13"},{"cycle":"2.4","eol":"2017-10-13","latest":"2.4.4","latestReleaseDate":"2017-09-14","releaseDate":"2017-07-13"},{"cycle":"2.3","eol":"2017-07-13","latest":"2.3.4","latestReleaseDate":"2017-06-08","releaseDate":"2017-04-27"},{"cycle":"2.2","eol":"2017-04-27","latest":"2.2.6","latestReleaseDate":"2017-03-27","releaseDate":"2017-02-26"},{"cycle":"2.1","eol":"2017-02-26","latest":"2.1.10","latestReleaseDate":"2017-01-17","releaseDate":"2016-11-22"},{"cycle":"2.0","eol":"2016-11-22","latest":"2.0.8","latestReleaseDate":"2016-11-20","releaseDate":"2016-09-30"},{"cycle":"1","eol":true,"latest":"1.0.28","latestReleaseDate":"2016-09-27","releaseDate":"2015-10-27"}]} diff --git a/apps/backend/tasks/scan_source.py b/apps/backend/tasks/scan_source.py index 4f70cbb2..cbad42f4 100644 --- a/apps/backend/tasks/scan_source.py +++ b/apps/backend/tasks/scan_source.py @@ -73,6 +73,7 @@ from core.config import ( cdxgen_fetch_license, cdxgen_spec_version, + eol_enabled, scan_scope_filter_enabled, scan_scope_filter_maven_enabled, scan_scope_filter_node_enabled, @@ -120,6 +121,7 @@ VulnerabilityFinding, ) from services.component_approval_service import auto_create_pending_approvals +from services.eol import eol_catalog from services.license_expression import evaluate_expression from services.license_normalize import normalize_license_name from services.sbom_conformance import sanitize_jsonb_text @@ -2271,6 +2273,13 @@ def persist_sbom_components( # F-4: aggregated telemetry — one warning per scan, not per component. sanitized_count = 0 deduplicated_count = 0 + # Phase M — EOL enrichment. One evaluator per persist call (rules + + # snapshot loaded once). None when disabled or the dataset failed to + # load — stamping silently skips (an EOL failure never breaks persist). + # Covers BOTH the source-scan and SBOM-ingest paths (they converge here). + eol_evaluator = eol_catalog.build_evaluator() if eol_enabled() else None + eol_stamped_count = 0 + eol_now = datetime.now(UTC) for raw in components: if not isinstance(raw, dict): @@ -2305,6 +2314,23 @@ def persist_sbom_components( purl_with_version=purl, ) + # Phase M — stamp the shared catalog row with its endoflife.date + # verdict. Changed-value-guarded (idempotent on re-scan); an + # unmapped purl returns None and leaves every eol_* column NULL. + if eol_evaluator is not None: + try: + if eol_catalog.stamp_component_version( + component_version, + eol_evaluator.verdict_for(purl, version), + eol_now, + ): + eol_stamped_count += 1 + except Exception: # noqa: BLE001 — enrichment is best-effort + eol_evaluator = None # disable for the rest of the scan + log.warning( + "eol_stamp_failed", scan_id=str(scan_uuid), exc_info=True + ) + bom_ref = raw.get("bom-ref") # TEXT column — persist the CLEANED ref; the raw one keys the maps. # F-3: a non-string bom-ref carries no path information → NULL. @@ -2404,6 +2430,14 @@ def persist_sbom_components( deduplicated_components=deduplicated_count, ) + if eol_stamped_count: + # Aggregate INFO — one line per scan, not per component (F-4 idiom). + log.info( + "eol_stamped", + scan_id=str(scan_uuid), + stamped_component_versions=eol_stamped_count, + ) + # v2.2 2.2-a2 — stamp depth + persist the resolved dependency edges. Runs # after the component loop so every ref the graph might reference is already # mapped. Best-effort and isolated: a malformed graph degrades to "no depth / diff --git a/apps/backend/tests/fixtures/eol/bomlens-eol-purl-map.json b/apps/backend/tests/fixtures/eol/bomlens-eol-purl-map.json new file mode 100644 index 00000000..b28ff4ce --- /dev/null +++ b/apps/backend/tests/fixtures/eol/bomlens-eol-purl-map.json @@ -0,0 +1,22 @@ +{ + "_comment": "Component PURL coordinate -> endoflife.date product, for offline end-of-life (EOL) flagging (enrich-eol.sh). A component whose purl starts with a rule's `purlPrefix` is mapped to that endoflife product; its version's leading segments form the release `cycle` (per the rule's `cycle` granularity), which is looked up in the bundled endoflife dataset (eol-data.json) to decide EOL. This is a CLOSED, curated whitelist on purpose: an unmapped component gets bomlens:eol=unknown, never a guess. Match is by PURL, not display name, because one product arrives under many component names (e.g. spring-boot-starter-web, spring-boot-autoconfigure all map to spring-boot). endoflife.date coverage is runtimes/frameworks/OS, so long-tail libraries are intentionally absent. Keep this map and eol-data.json's product set in sync.", + + "_cycle": "cycle granularity: 'major' uses the first numeric version segment (express 4.18.2 -> '4'); 'major.minor' uses the first two joined by '.' (spring-boot 3.2.0 -> '3.2'). Must match how endoflife.date keys that product's cycles.", + + "rules": [ + { "purlPrefix": "pkg:maven/org.springframework.boot/", "product": "spring-boot", "cycle": "major.minor" }, + { "purlPrefix": "pkg:maven/org.springframework/", "product": "spring-framework", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:npm/express@", "product": "express", "cycle": "major" }, + { "purlPrefix": "pkg:npm/next@", "product": "nextjs", "cycle": "major.minor" }, + { "purlPrefix": "pkg:npm/@angular/core@", "product": "angular", "cycle": "major" }, + { "purlPrefix": "pkg:npm/vue@", "product": "vue", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:pypi/django@", "product": "django", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:gem/rails@", "product": "rails", "cycle": "major.minor" }, + + { "purlPrefix": "pkg:composer/symfony/symfony@", "product": "symfony", "cycle": "major.minor" }, + { "purlPrefix": "pkg:composer/laravel/framework@", "product": "laravel", "cycle": "major" } + ] +} diff --git a/apps/backend/tests/fixtures/eol/eol-components.json b/apps/backend/tests/fixtures/eol/eol-components.json new file mode 100644 index 00000000..09c802c8 --- /dev/null +++ b/apps/backend/tests/fixtures/eol/eol-components.json @@ -0,0 +1,20 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "components": [ + { "type": "library", "name": "spring-boot-starter-web", "version": "3.2.0", + "purl": "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0?type=jar" }, + { "type": "library", "name": "spring-boot-actuator", "version": "3.3.1", + "purl": "pkg:maven/org.springframework.boot/spring-boot-actuator@3.3.1?type=jar" }, + { "type": "library", "name": "spring-boot-experimental", "version": "9.9.0", + "purl": "pkg:maven/org.springframework.boot/spring-boot-experimental@9.9.0?type=jar" }, + { "type": "library", "name": "express", "version": "4.18.2", + "purl": "pkg:npm/express@4.18.2" }, + { "type": "library", "name": "django", "version": "4.2.1", + "purl": "pkg:pypi/django@4.2.1" }, + { "type": "library", "name": "lodash", "version": "4.17.21", + "purl": "pkg:npm/lodash@4.17.21" }, + { "type": "library", "name": "express-session", "version": "1.17.3", + "purl": "pkg:npm/express-session@1.17.3" } + ] +} diff --git a/apps/backend/tests/fixtures/eol/eol-data.json b/apps/backend/tests/fixtures/eol/eol-data.json new file mode 100644 index 00000000..a4da055f --- /dev/null +++ b/apps/backend/tests/fixtures/eol/eol-data.json @@ -0,0 +1,15 @@ +{ + "_comment": "Deterministic endoflife.date-shaped fixture for tests/test-postprocess.sh (enrich-eol.sh). Dates are far past / far future so EOL true/false never depends on the run date. NOT the bundled snapshot (that is built from the live API at image build).", + "_snapshot": "2026-01-01", + "spring-boot": [ + { "cycle": "3.2", "eol": "2020-01-01", "latest": "3.2.12" }, + { "cycle": "3.3", "eol": "2099-12-31", "latest": "3.3.5" } + ], + "express": [ + { "cycle": "3", "eol": true }, + { "cycle": "4", "eol": false, "latest": "4.21.0" } + ], + "django": [ + { "cycle": "4.2", "eol": "2020-01-01", "latest": "4.2.16" } + ] +} diff --git a/apps/backend/tests/unit/services/test_eol_catalog.py b/apps/backend/tests/unit/services/test_eol_catalog.py new file mode 100644 index 00000000..5aec768a --- /dev/null +++ b/apps/backend/tests/unit/services/test_eol_catalog.py @@ -0,0 +1,315 @@ +"""Unit tests — EOL evaluation logic (Phase M, services/eol/eol_catalog.py). + +Fixtures are the BomLens originals (``tests/fixtures/eol/eol-data.json`` / +``eol-components.json``, vendored verbatim): the dataset's dates are far +past / far future so verdicts never depend on the run date, and the +component set exercises the exact edges the BomLens e2e verified — +including ``express-session``, which must NOT match the ``pkg:npm/express@`` +prefix (the ``@`` terminator prevents over-matching). +""" + +from __future__ import annotations + +import json +from datetime import date, datetime +from pathlib import Path + +import pytest + +from services.eol.eol_catalog import ( + EolDataset, + EolRule, + derive_cycle, + evaluate, + load_dataset, + load_rules, + stamp_component_version, +) + +FIXTURES = Path(__file__).resolve().parents[2] / "fixtures" / "eol" +TODAY = date(2026, 7, 11) + + +def _dataset() -> EolDataset: + raw = json.loads((FIXTURES / "eol-data.json").read_text(encoding="utf-8")) + return EolDataset( + snapshot=raw["_snapshot"], + products={k: v for k, v in raw.items() if not k.startswith("_")}, + ) + + +def _rules() -> tuple[EolRule, ...]: + return load_rules() + + +# --------------------------------------------------------------------------- +# derive_cycle — table (BomLens enrich-eol.sh:77-85 semantics) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("version", "granularity", "expected"), + [ + ("3.2.0", "major.minor", "3.2"), + ("4.18.2", "major", "4"), + ("v1.2.3", "major.minor", "1.2"), # leading v stripped + ("1.2.3-rc1", "major.minor", "1.2"), + ("1.2.3-rc1", "major", "1"), + ("5", "major.minor", "5"), # single segment → major fallback + ("5", "major", "5"), + ("2rc1.4", "major.minor", "2.4"), # leading-numeric part of each segment + ("1.x.3", "major.minor", "1"), # stops at first non-numeric-lead segment + ("rc1", "major", None), # fully non-numeric lead → unknown + ("", "major", None), + ("0.0.0", "major.minor", "0.0"), # persist default version + ], +) +def test_derive_cycle_table( + version: str, granularity: str, expected: str | None +) -> None: + assert derive_cycle(version, granularity) == expected + + +# --------------------------------------------------------------------------- +# evaluate — decision matrix +# --------------------------------------------------------------------------- + + +def test_dated_eol_in_the_past_is_eol() -> None: + verdict = evaluate( + "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0?type=jar", + "3.2.0", + rules=_rules(), + dataset=_dataset(), + today=TODAY, + ) + assert verdict is not None + assert verdict.state == "eol" + assert verdict.product == "spring-boot" + assert verdict.cycle == "3.2" + assert verdict.date == date(2020, 1, 1) + assert verdict.source == "endoflife.date@2026-01-01" + + +def test_dated_eol_in_the_future_is_supported() -> None: + verdict = evaluate( + "pkg:maven/org.springframework.boot/spring-boot-actuator@3.3.1", + "3.3.1", + rules=_rules(), + dataset=_dataset(), + today=TODAY, + ) + assert verdict is not None + assert verdict.state == "supported" + assert verdict.date == date(2099, 12, 31) + + +def test_boolean_eol_values() -> None: + eol_true = evaluate( + "pkg:npm/express@3.1.0", "3.1.0", + rules=_rules(), dataset=_dataset(), today=TODAY, + ) + assert eol_true is not None and eol_true.state == "eol" + assert eol_true.date is None # boolean feeds carry no date + + eol_false = evaluate( + "pkg:npm/express@4.18.2", "4.18.2", + rules=_rules(), dataset=_dataset(), today=TODAY, + ) + assert eol_false is not None and eol_false.state == "supported" + + +def test_unlisted_cycle_is_unknown() -> None: + verdict = evaluate( + "pkg:maven/org.springframework.boot/spring-boot-experimental@9.9.0", + "9.9.0", + rules=_rules(), + dataset=_dataset(), + today=TODAY, + ) + assert verdict is not None + assert verdict.state == "unknown" + assert verdict.cycle == "9.9" + + +def test_underivable_version_is_unknown_with_null_cycle() -> None: + verdict = evaluate( + "pkg:npm/express@latest", "latest", + rules=_rules(), dataset=_dataset(), today=TODAY, + ) + assert verdict is not None + assert verdict.state == "unknown" + assert verdict.cycle is None + + +def test_malformed_date_string_is_unknown() -> None: + dataset = EolDataset( + snapshot="2026-01-01", + products={"express": [{"cycle": "4", "eol": "not-a-date"}]}, + ) + verdict = evaluate( + "pkg:npm/express@4.1.0", "4.1.0", + rules=_rules(), dataset=dataset, today=TODAY, + ) + assert verdict is not None and verdict.state == "unknown" + + +def test_numeric_cycle_in_feed_matches_via_str() -> None: + # endoflife.date sometimes emits numeric cycles (4 not "4"). + dataset = EolDataset( + snapshot="2026-01-01", products={"express": [{"cycle": 4, "eol": False}]} + ) + verdict = evaluate( + "pkg:npm/express@4.1.0", "4.1.0", + rules=_rules(), dataset=dataset, today=TODAY, + ) + assert verdict is not None and verdict.state == "supported" + + +# --------------------------------------------------------------------------- +# Matching — closed whitelist, no over-match, %40 normalisation +# --------------------------------------------------------------------------- + + +def test_unmapped_component_returns_none() -> None: + for purl, version in ( + ("pkg:npm/lodash@4.17.21", "4.17.21"), + ("pkg:pypi/requests@2.31.0", "2.31.0"), + ): + assert ( + evaluate(purl, version, rules=_rules(), dataset=_dataset(), today=TODAY) + is None + ) + + +def test_express_session_does_not_match_the_express_rule() -> None: + # The trailing @ in "pkg:npm/express@" is the over-match terminator. + assert ( + evaluate( + "pkg:npm/express-session@1.17.3", + "1.17.3", + rules=_rules(), + dataset=_dataset(), + today=TODAY, + ) + is None + ) + + +def test_url_encoded_scope_normalised_before_match() -> None: + # cdxgen emits pkg:npm/%40angular/core@17.0.0; the map says @angular. + dataset = EolDataset( + snapshot="2026-01-01", products={"angular": [{"cycle": "17", "eol": True}]} + ) + verdict = evaluate( + "pkg:npm/%40angular/core@17.0.0", + "17.0.0", + rules=_rules(), + dataset=dataset, + today=TODAY, + ) + assert verdict is not None + assert verdict.product == "angular" + assert verdict.state == "eol" + + +def test_purl_qualifiers_do_not_break_prefix_match() -> None: + # "?type=jar" trails the version — prefix matching is unaffected. + verdict = evaluate( + "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0?type=jar", + "3.2.0", + rules=_rules(), + dataset=_dataset(), + today=TODAY, + ) + assert verdict is not None and verdict.state == "eol" + + +# --------------------------------------------------------------------------- +# load_dataset — override + degradation +# --------------------------------------------------------------------------- + + +def test_load_dataset_env_override( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + override = tmp_path / "snap.json" + override.write_text( + json.dumps({"_snapshot": "2026-06-01", "express": [{"cycle": "4", "eol": False}]}), + encoding="utf-8", + ) + monkeypatch.setenv("EOL_SNAPSHOT_PATH", str(override)) + dataset = load_dataset() + assert dataset is not None + assert dataset.snapshot == "2026-06-01" + + +@pytest.mark.parametrize( + "content", + [ + "{ not json", + json.dumps(["wrong", "shape"]), + json.dumps({"express": []}), # missing _snapshot + json.dumps({"_snapshot": "2026-01-01"}), # no products + ], +) +def test_load_dataset_corrupt_returns_none( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, content: str +) -> None: + bad = tmp_path / "snap.json" + bad.write_text(content, encoding="utf-8") + monkeypatch.setenv("EOL_SNAPSHOT_PATH", str(bad)) + assert load_dataset() is None + + +def test_load_dataset_missing_override_returns_none( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("EOL_SNAPSHOT_PATH", str(tmp_path / "absent.json")) + assert load_dataset() is None + + +def test_vendored_dataset_loads(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("EOL_SNAPSHOT_PATH", raising=False) + dataset = load_dataset() + assert dataset is not None + assert dataset.cycles("spring-boot") # vendored snapshot carries the map's products + + +# --------------------------------------------------------------------------- +# stamp_component_version — changed-value guard +# --------------------------------------------------------------------------- + + +class _FakeComponentVersion: + eol_state: str | None = None + eol_product: str | None = None + eol_cycle: str | None = None + eol_date: date | None = None + eol_source: str | None = None + eol_evaluated_at: datetime | None = None + + +def test_stamp_writes_then_second_stamp_is_a_noop() -> None: + row = _FakeComponentVersion() + verdict = evaluate( + "pkg:npm/express@3.1.0", "3.1.0", + rules=_rules(), dataset=_dataset(), today=TODAY, + ) + now = datetime(2026, 7, 11, 12, 0, 0) + assert stamp_component_version(row, verdict, now) is True # type: ignore[arg-type] + assert row.eol_state == "eol" + assert row.eol_evaluated_at == now + + later = datetime(2026, 7, 12, 12, 0, 0) + assert stamp_component_version(row, verdict, later) is False # type: ignore[arg-type] + assert row.eol_evaluated_at == now # unchanged row not re-dirtied + + +def test_stamp_none_verdict_leaves_row_untouched() -> None: + row = _FakeComponentVersion() + assert ( + stamp_component_version(row, None, datetime(2026, 7, 11)) is False # type: ignore[arg-type] + ) + assert row.eol_state is None + assert row.eol_evaluated_at is None diff --git a/apps/backend/tests/unit/services/test_eol_catalog_contracts.py b/apps/backend/tests/unit/services/test_eol_catalog_contracts.py new file mode 100644 index 00000000..1a9740e9 --- /dev/null +++ b/apps/backend/tests/unit/services/test_eol_catalog_contracts.py @@ -0,0 +1,74 @@ +"""Consistency-contract tests — vendored EOL purl map (Phase M). + +The G7 registry precedent (CLAUDE.md hardening rule 2): the same vocabulary +living in two places needs a set-equality contract, or the copies drift while +each side's own tests stay green. Three copies exist here: + + 1. ``services/eol/eol_purl_map.json`` — the map the evaluator runs on, + vendored VERBATIM from BomLens ``docker/lib/eol-purl-map.json``; + 2. ``tests/fixtures/eol/bomlens-eol-purl-map.json`` — the captured BomLens + original (byte-comparison anchor; refresh it when re-vendoring); + 3. ``services/eol/eol_snapshot.json`` — the dataset, whose product set the + map's own ``_comment`` demands stays in sync with the rules. +""" + +from __future__ import annotations + +import json +from datetime import date +from pathlib import Path + +from services.eol.eol_catalog import load_rules + +_BACKEND = Path(__file__).resolve().parents[3] +VENDORED_MAP = _BACKEND / "services" / "eol" / "eol_purl_map.json" +CAPTURED_MAP = _BACKEND / "tests" / "fixtures" / "eol" / "bomlens-eol-purl-map.json" +VENDORED_SNAPSHOT = _BACKEND / "services" / "eol" / "eol_snapshot.json" + + +def test_vendored_map_is_byte_identical_to_the_captured_bomlens_copy() -> None: + # Verbatim vendoring is the contract: the %40 npm-scope divergence is + # absorbed in the MATCHER (eol_catalog.evaluate), never in the map, so + # re-vendoring stays a plain file copy. + assert VENDORED_MAP.read_bytes() == CAPTURED_MAP.read_bytes() + + +def test_map_products_and_snapshot_products_are_set_equal() -> None: + map_products = { + rule["product"] + for rule in json.loads(VENDORED_MAP.read_text(encoding="utf-8"))["rules"] + } + snapshot = json.loads(VENDORED_SNAPSHOT.read_text(encoding="utf-8")) + snapshot_products = {k for k in snapshot if not k.startswith("_")} + assert map_products == snapshot_products, ( + "eol_purl_map.json rules and eol_snapshot.json products drifted — " + "run scripts/refresh_eol_snapshot.py after changing the map" + ) + + +def test_snapshot_stamp_is_an_iso_date() -> None: + snapshot = json.loads(VENDORED_SNAPSHOT.read_text(encoding="utf-8")) + date.fromisoformat(snapshot["_snapshot"]) # raises on drift + + +def test_loader_accepts_every_vendored_rule() -> None: + # load_rules drops malformed entries silently; the vendored map must + # survive intact (a dropped rule would silently stop flagging a product). + raw_rules = json.loads(VENDORED_MAP.read_text(encoding="utf-8"))["rules"] + assert len(load_rules()) == len(raw_rules) + + +def test_every_snapshot_cycle_entry_is_evaluable() -> None: + # Every entry must carry a cycle, and its eol value must be one of the + # shapes evaluate() understands (bool / ISO date string / absent). + snapshot = json.loads(VENDORED_SNAPSHOT.read_text(encoding="utf-8")) + for product, cycles in snapshot.items(): + if product.startswith("_"): + continue + for entry in cycles: + assert "cycle" in entry, f"{product}: entry without cycle" + eol_value = entry.get("eol") + if isinstance(eol_value, str): + date.fromisoformat(eol_value) + else: + assert eol_value is None or isinstance(eol_value, bool) diff --git a/apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py b/apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py new file mode 100644 index 00000000..6ee615cf --- /dev/null +++ b/apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py @@ -0,0 +1,169 @@ +"""Unit tests — EOL stamping inside ``persist_sbom_components`` (Phase M). + +The SBOM fixture is the BomLens original (``tests/fixtures/eol/ +eol-components.json``) and the dataset is its deterministic companion +(``eol-data.json``, far past/future dates) — the exact pair the BomLens +post-process e2e verified. Expected verdicts: + + spring-boot-starter-web@3.2.0 → eol (dated 2020-01-01, past) + spring-boot-actuator@3.3.1 → supported (dated 2099-12-31, future) + spring-boot-experimental@9.9.0 → unknown (cycle unlisted) + express@4.18.2 → supported (boolean false) + django@4.2.1 → eol (dated past) + lodash@4.17.21 → untouched (unmapped — every column NULL) + express-session@1.17.3 → untouched (must NOT match express rule) + +Also pinned: rerun idempotency (changed-value guard), EOL_ENABLED=false +skips, corrupt dataset degrades to no stamping, ingest-path coverage is +automatic (same persist function). +""" + +from __future__ import annotations + +import json +import uuid +from datetime import date +from pathlib import Path +from typing import Any + +import pytest + +FIXTURES = Path(__file__).resolve().parents[2] / "fixtures" / "eol" +SBOM_PATH = FIXTURES / "eol-components.json" +DATA_PATH = FIXTURES / "eol-data.json" + + +class _FakeComponent: + def __init__(self) -> None: + self.id = uuid.uuid4() + + +class _FakeComponentVersion: + def __init__(self, purl: str) -> None: + self.id = uuid.uuid4() + self.purl_with_version = purl + self.eol_state: str | None = None + self.eol_product: str | None = None + self.eol_cycle: str | None = None + self.eol_date: date | None = None + self.eol_source: str | None = None + self.eol_evaluated_at = None + + +class _FakeSession: + def __init__(self) -> None: + self.added: list[Any] = [] + + def add(self, row: Any) -> None: + self.added.append(row) + + +@pytest.fixture +def cv_registry(monkeypatch: pytest.MonkeyPatch) -> dict[str, _FakeComponentVersion]: + """Stub persist helpers; capture ComponentVersion fakes keyed by purl. + + Reuses the same fake CV for a repeated purl — mirroring the real + ``_get_or_create_component_version`` (shared catalog row), which is what + makes the rerun-idempotency assertion meaningful. + """ + registry: dict[str, _FakeComponentVersion] = {} + + def _get_cv( + session: Any, *, component: Any, version: str, purl_with_version: str + ) -> _FakeComponentVersion: + if purl_with_version not in registry: + registry[purl_with_version] = _FakeComponentVersion(purl_with_version) + return registry[purl_with_version] + + monkeypatch.setattr( + "tasks.scan_source._get_or_create_component", + lambda session, *, purl, name, package_type: _FakeComponent(), + ) + monkeypatch.setattr("tasks.scan_source._get_or_create_component_version", _get_cv) + monkeypatch.setattr( + "tasks.scan_source._persist_component_licenses", + lambda session, *, scan_uuid, component_version_id, cdxgen_component, purl: None, + ) + monkeypatch.setattr( + "tasks.scan_source._persist_dependency_graph", + lambda session, **kwargs: None, + ) + return registry + + +@pytest.fixture +def fixture_dataset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("EOL_SNAPSHOT_PATH", str(DATA_PATH)) + + +def _sbom() -> dict[str, Any]: + return json.loads(SBOM_PATH.read_text(encoding="utf-8")) + + +def _persist(session: _FakeSession) -> None: + from tasks.scan_source import persist_sbom_components + + persist_sbom_components(session, scan_uuid=uuid.uuid4(), sbom=_sbom()) + + +def _by_name(registry: dict[str, _FakeComponentVersion]) -> dict[str, _FakeComponentVersion]: + return {purl.split("/")[-1].split("@")[0]: cv for purl, cv in registry.items()} + + +def test_fixture_sbom_stamps_the_expected_verdict_matrix( + cv_registry: dict[str, _FakeComponentVersion], fixture_dataset: None +) -> None: + _persist(_FakeSession()) + rows = _by_name(cv_registry) + + assert rows["spring-boot-starter-web"].eol_state == "eol" + assert rows["spring-boot-starter-web"].eol_date == date(2020, 1, 1) + assert rows["spring-boot-starter-web"].eol_product == "spring-boot" + assert rows["spring-boot-starter-web"].eol_cycle == "3.2" + assert rows["spring-boot-starter-web"].eol_source == "endoflife.date@2026-01-01" + + assert rows["spring-boot-actuator"].eol_state == "supported" + assert rows["spring-boot-experimental"].eol_state == "unknown" + assert rows["express"].eol_state == "supported" + assert rows["django"].eol_state == "eol" + + # Unmapped / near-miss components stay untouched — every column NULL. + for untouched in ("lodash", "express-session"): + row = rows[untouched] + assert row.eol_state is None + assert row.eol_product is None + assert row.eol_evaluated_at is None + + +def test_rerun_is_idempotent( + cv_registry: dict[str, _FakeComponentVersion], fixture_dataset: None +) -> None: + _persist(_FakeSession()) + first_stamp = _by_name(cv_registry)["spring-boot-starter-web"].eol_evaluated_at + assert first_stamp is not None + + _persist(_FakeSession()) # same catalog rows re-observed (shared registry) + second_stamp = _by_name(cv_registry)["spring-boot-starter-web"].eol_evaluated_at + assert second_stamp == first_stamp # changed-value guard: not re-dirtied + + +def test_eol_disabled_skips_stamping( + cv_registry: dict[str, _FakeComponentVersion], + fixture_dataset: None, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("EOL_ENABLED", "false") + _persist(_FakeSession()) + assert all(cv.eol_state is None for cv in cv_registry.values()) + + +def test_corrupt_dataset_degrades_to_no_stamping( + cv_registry: dict[str, _FakeComponentVersion], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + bad = tmp_path / "corrupt.json" + bad.write_text("{ not json", encoding="utf-8") + monkeypatch.setenv("EOL_SNAPSHOT_PATH", str(bad)) + _persist(_FakeSession()) # must not raise + assert all(cv.eol_state is None for cv in cv_registry.values()) From 4198757813f3cda5a6273a6167cee6d4d323ed2a Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 01:20:23 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat(fe,api):=20surface=20end-of-life=20v?= =?UTF-8?q?erdicts=20=E2=80=94=20column,=20badge,=20filter,=20KPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the catalog-layer EOL data (previous commit) end to end: - API: ComponentSummary carries eol_state/eol_date and the detail response the full verdict (product/cycle/date/source) — free columns on the existing ComponentVersion joins, no extra query. New ?eol=true|false facet rides the partial index; the overview gains eol_count (one count query gathered with the existing aggregation). OpenAPI snapshot regenerated for the new param. - FE: EolBadge (KevBadge structural mirror — High tone, renders only for state 'eol', absence is the signal), EOL column in the components table, an 'EOL only' toolbar toggle mirrored to ?eol=true, an always-rendered drawer row that distinguishes untracked (blank) from unknown (tracked, cycle undecidable), and an Overview chip deep-linking to the filtered list. EN/KO keys in the same change. - Contracts: the closed eol_state vocabulary now lives in three places (catalog tuple, wire Literals, FE mirror) — set-equality tests on both sides (test_catalog_contracts.py + catalogMirrors.test.ts) plus EN/KO label coverage. - e2e: components_eol.spec.ts (badge / filter+URL / drawer / overview chip); the seed stamps a far-past EOL verdict on exactly the first seeded component so assertions never depend on the run date or the vendored snapshot's live contents. - Docs EN/KO: end-of-life section in the components guide (blank-vs-unknown semantics), endoflife.date row in data-sources, EOL_* env vars. --- CHANGELOG.md | 10 ++ apps/backend/api/v1/projects.py | 11 ++ apps/backend/schemas/project_detail.py | 52 ++++++- apps/backend/scripts/seed_e2e_user.py | 15 +- .../services/project_detail_service.py | 64 +++++++- .../backend/tests/unit/openapi_endpoints.json | 1 + .../tests/unit/test_catalog_contracts.py | 31 ++++ .../features/projects/api/projectDetailApi.ts | 44 ++++++ .../features/projects/api/useComponents.ts | 7 + .../components/ComponentDetailBody.tsx | 32 ++++ .../projects/components/ComponentsTab.tsx | 28 ++++ .../projects/components/ComponentsToolbar.tsx | 31 ++++ .../features/projects/components/EolBadge.tsx | 79 ++++++++++ .../projects/components/OverviewTab.tsx | 39 +++++ .../src/locales/en/project_detail.json | 23 ++- .../src/locales/ko/project_detail.json | 23 ++- .../frontend/tests/e2e/components_eol.spec.ts | 138 ++++++++++++++++++ .../components/ComponentDetailBody.test.tsx | 5 + .../unit/contracts/catalogMirrors.test.ts | 27 ++++ .../projects/ComponentDrawer.test.tsx | 5 + .../features/projects/ComponentsTab.test.tsx | 7 + .../features/projects/OverviewTab.test.tsx | 1 + .../projects/ProjectDetailPage.test.tsx | 1 + .../unit/pages/ComponentDetailPage.test.tsx | 5 + docs-site/docs/reference/data-sources.md | 6 + docs-site/docs/reference/env-variables.md | 2 + .../user-guide/components-and-licenses.md | 33 +++++ .../current/reference/data-sources.md | 6 + .../current/reference/env-variables.md | 2 + .../user-guide/components-and-licenses.md | 33 +++++ 30 files changed, 756 insertions(+), 5 deletions(-) create mode 100644 apps/frontend/src/features/projects/components/EolBadge.tsx create mode 100644 apps/frontend/tests/e2e/components_eol.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a937a2..e0ff1b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- **End-of-life (EOL) component flagging.** Components matching a curated + endoflife.date product whitelist (Spring Boot, Express, Django, Rails, + Angular, Vue, Next.js, Symfony, Laravel, Spring Framework) are stamped + with their lifecycle verdict on the shared catalog. The Components tab + gains an EOL column/badge and an "EOL only" filter (`?eol=true`), the + drawer an End-of-life row, and the project Overview an EOL count that + deep-links to the filtered list. Verdicts come from a snapshot vendored + with the release — zero network at scan time, air-gap safe + (`EOL_SNAPSHOT_PATH` mounts a fresher snapshot; `EOL_ENABLED=false` + disables). - **iOS CocoaPods/SPM lockfile scanning.** A `Podfile` used to crash the whole source scan (cdxgen's cocoapods cataloger throws without the `pod` CLI). The scanner now excludes that cataloger and reconstructs pods — diff --git a/apps/backend/api/v1/projects.py b/apps/backend/api/v1/projects.py index 759bde83..9f7ab4aa 100644 --- a/apps/backend/api/v1/projects.py +++ b/apps/backend/api/v1/projects.py @@ -506,6 +506,16 @@ async def list_project_components_endpoint( "values returns an empty page (not a 422). Omit to include all." ), ), + eol: bool | None = Query( + default=None, + description=( + "Phase M — end-of-life facet. ``true`` keeps only components " + "whose release cycle is past its published end-of-life " + "(endoflife.date); ``false`` keeps everything else, including " + "untracked components. Omit to include both. Boolean mirrors " + "the KEV filter UX." + ), + ), sort: str = Query(default="name", pattern=r"^(name|severity|license)$"), order: str = Query(default="asc", pattern=r"^(asc|desc)$"), scan_id: uuid.UUID | None = Query( @@ -532,6 +542,7 @@ async def list_project_components_endpoint( license_category=license_category, direct=direct, dependency_scope=dependency_scope, + eol=eol, sort=sort, order=order, scan_id=scan_id, diff --git a/apps/backend/schemas/project_detail.py b/apps/backend/schemas/project_detail.py index 325aa3a5..6161ef9c 100644 --- a/apps/backend/schemas/project_detail.py +++ b/apps/backend/schemas/project_detail.py @@ -41,7 +41,7 @@ from __future__ import annotations import uuid -from datetime import datetime +from datetime import date, datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -128,6 +128,15 @@ class ProjectOverviewResponse(BaseModel): project_id: uuid.UUID project_name: str total_components: int + eol_count: int = Field( + default=0, + description=( + "Phase M — count of distinct components in the anchored scan " + "whose release cycle is past its published end-of-life " + "(endoflife.date). A supply-chain risk axis distinct from CVEs: " + "an EOL runtime gets no upstream fixes." + ), + ) severity_distribution: dict[str, int] = Field( default_factory=dict, description=( @@ -280,6 +289,24 @@ class ComponentSummary(BaseModel): "guessing." ), ) + eol_state: Literal["eol", "supported", "unknown"] | None = Field( + default=None, + description=( + "Phase M — endoflife.date verdict for this component version. " + "``eol`` = release cycle past its published end-of-life; " + "``supported`` = tracked and still supported; ``unknown`` = " + "tracked product but the cycle could not be decided; ``null`` = " + "not a tracked product (closed whitelist — never guessed). The " + "UI renders a badge only for ``eol`` (absence is the signal)." + ), + ) + eol_date: date | None = Field( + default=None, + description=( + "Published end-of-life date, when the feed carries one (dated " + "cycles). ``null`` for boolean-only feeds and untracked rows." + ), + ) class ComponentListResponse(BaseModel): @@ -414,6 +441,29 @@ class ComponentDetailResponse(BaseModel): "depth/direct already pin one path." ), ) + eol_state: Literal["eol", "supported", "unknown"] | None = Field( + default=None, + description=( + "Phase M — endoflife.date verdict (see ComponentSummary.eol_state " + "for the vocabulary). ``null`` = not a tracked product." + ), + ) + eol_product: str | None = Field( + default=None, + description="endoflife.date product slug the component mapped to.", + ) + eol_cycle: str | None = Field( + default=None, + description="Release cycle derived from the version (e.g. '3.2').", + ) + eol_date: date | None = Field( + default=None, + description="Published end-of-life date, when the feed carries one.", + ) + eol_source: str | None = Field( + default=None, + description="Snapshot provenance, e.g. 'endoflife.date@2026-07-11'.", + ) created_at: datetime updated_at: datetime diff --git a/apps/backend/scripts/seed_e2e_user.py b/apps/backend/scripts/seed_e2e_user.py index dacc6c55..3cf955af 100644 --- a/apps/backend/scripts/seed_e2e_user.py +++ b/apps/backend/scripts/seed_e2e_user.py @@ -133,7 +133,7 @@ import os import sys import uuid -from datetime import UTC, datetime, timedelta +from datetime import UTC, date, datetime, timedelta from decimal import Decimal from pathlib import Path from typing import TYPE_CHECKING @@ -1403,6 +1403,19 @@ async def _seed( # noqa: PLR0915 — a single linear seed routine reads better version="1.0.0", purl_with_version=f"{purl}@1.0.0", ) + if i == 0: + # Phase M — deterministic EOL fixture: the FIRST seeded + # component carries a past end-of-life verdict (columns + # written directly — the far-past date can never flip + # with the run date, and e2e stays decoupled from the + # vendored snapshot's live contents). Every other row + # stays NULL = untracked (badge absent — the contract). + cv.eol_state = "eol" + cv.eol_product = "seed-product" + cv.eol_cycle = "1.0" + cv.eol_date = date(2020, 1, 1) + cv.eol_source = "endoflife.date@seed" + cv.eol_evaluated_at = datetime.now(tz=UTC) session.add(cv) await session.flush() seeded_cv_ids.append(cv.id) diff --git a/apps/backend/services/project_detail_service.py b/apps/backend/services/project_detail_service.py index 41985c2a..614be64d 100644 --- a/apps/backend/services/project_detail_service.py +++ b/apps/backend/services/project_detail_service.py @@ -314,6 +314,7 @@ async def get_project_overview( severity_distribution = dict.fromkeys(_ALL_SEVERITY_KEYS, 0) license_distribution = dict.fromkeys(_ALL_LICENSE_KEYS, 0) total_components = 0 + eol_count = 0 last_scan_at: Any = None last_succeeded_scan_at: Any = None # #35 Surface B — tri-state: True/False once we know the DT vuln-DB size at @@ -414,11 +415,33 @@ async def get_project_overview( Scan.id == aggregate_scan_id ) - agg_result, recent_result, succeeded_meta_result = await asyncio.gather( + # Phase M — EOL rollup for the Overview KPI chip: distinct catalog + # rows observed in the anchor scan that are past end-of-life. Rides + # the partial index ix_component_versions_eol (flagged rows are a + # tiny curated minority). + eol_count_stmt = ( + select(func.count(func.distinct(ScanComponent.component_version_id))) + .select_from(ScanComponent) + .join( + ComponentVersion, + ComponentVersion.id == ScanComponent.component_version_id, + ) + .where(ScanComponent.scan_id == aggregate_scan_id) + .where(ComponentVersion.eol_state == "eol") + ) + + ( + agg_result, + recent_result, + succeeded_meta_result, + eol_count_result, + ) = await asyncio.gather( session.execute(agg_stmt), session.execute(recent_stmt), session.execute(succeeded_meta_stmt), + session.execute(eol_count_stmt), ) + eol_count = int(eol_count_result.scalar_one()) succeeded_row = succeeded_meta_result.one_or_none() if succeeded_row is not None: @@ -462,6 +485,7 @@ async def get_project_overview( "project_id": project.id, "project_name": project.name, "total_components": total_components, + "eol_count": eol_count, "severity_distribution": severity_distribution, "license_distribution": license_distribution, "risk_score": overall_risk, @@ -537,6 +561,7 @@ async def list_components_for_project( license_category: list[str] | None = None, direct: bool | None = None, dependency_scope: list[str] | None = None, + eol: bool | None = None, sort: str = "name", order: str = "asc", scan_id: uuid.UUID | None = None, @@ -666,6 +691,10 @@ async def list_components_for_project( per_cv_subq.c.min_depth.label("min_depth"), per_cv_subq.c.is_direct.label("is_direct"), per_cv_subq.c.max_scope_rank.label("max_scope_rank"), + # Phase M — EOL rides the catalog row (component_versions), so the + # labels are free on the existing join; no extra query. + ComponentVersion.eol_state.label("eol_state"), + ComponentVersion.eol_date.label("eol_date"), ) .select_from(per_cv_subq) .join(ComponentVersion, ComponentVersion.id == per_cv_subq.c.cv_id) @@ -711,6 +740,21 @@ async def list_components_for_project( scope_ranks = [_SCOPE_FILTER_RANK[s] for s in scope_filter] base = base.where(per_cv_subq.c.max_scope_rank.in_(scope_ranks)) + # Phase M — EOL filter. ``eol=true`` keeps only flagged rows (served by + # the partial index ix_component_versions_eol); ``eol=false`` keeps + # everything else INCLUDING untracked (NULL) and unknown rows — the + # boolean maps to "past end-of-life or not", mirroring the KEV filter UX. + if eol is not None: + if eol: + base = base.where(ComponentVersion.eol_state == "eol") + else: + base = base.where( + or_( + ComponentVersion.eol_state.is_(None), + ComponentVersion.eol_state != "eol", + ) + ) + # Sorting. We pick the primary column then call .asc()/.desc() inline so # mypy --strict doesn't complain about an untyped lambda factory. primary: Any @@ -799,6 +843,11 @@ async def list_components_for_project( # SBOM didn't encode one); the UI renders that as "—" rather # than guessing. "dependency_scope": _SCOPE_FROM_RANK[int(r.max_scope_rank)], + # Phase M — endoflife.date verdict off the catalog row. NULL = + # not a tracked product (the FE renders nothing, KevBadge + # contract: absence is the signal). + "eol_state": r.eol_state, + "eol_date": r.eol_date, } ) @@ -837,6 +886,12 @@ async def get_component_detail( ComponentVersion.purl_with_version, ComponentVersion.created_at, ComponentVersion.updated_at, + # Phase M — EOL columns ride the catalog row; free on this select. + ComponentVersion.eol_state, + ComponentVersion.eol_product, + ComponentVersion.eol_cycle, + ComponentVersion.eol_date, + ComponentVersion.eol_source, Component.id.label("component_id"), Component.name.label("component_name"), Project.id.label("project_id"), @@ -1054,6 +1109,13 @@ async def get_component_detail( if row.dependency_scope in ("required", "optional") else None ), + # Phase M — endoflife.date verdict (catalog columns). All None for an + # untracked component; the drawer's EOL row renders "—" then. + "eol_state": row.eol_state, + "eol_product": row.eol_product, + "eol_cycle": row.eol_cycle, + "eol_date": row.eol_date, + "eol_source": row.eol_source, "created_at": row.created_at, "updated_at": row.updated_at, } diff --git a/apps/backend/tests/unit/openapi_endpoints.json b/apps/backend/tests/unit/openapi_endpoints.json index 90070f01..a069c3f8 100644 --- a/apps/backend/tests/unit/openapi_endpoints.json +++ b/apps/backend/tests/unit/openapi_endpoints.json @@ -199,6 +199,7 @@ "GET /v1/projects/{project_id}/components": [ "dependency_scope", "direct", + "eol", "license_category", "limit", "offset", diff --git a/apps/backend/tests/unit/test_catalog_contracts.py b/apps/backend/tests/unit/test_catalog_contracts.py index 45843a21..3583891e 100644 --- a/apps/backend/tests/unit/test_catalog_contracts.py +++ b/apps/backend/tests/unit/test_catalog_contracts.py @@ -257,3 +257,34 @@ def test_review_flag_router_pattern_matches_classifier_values() -> None: f"{len(review_alternations)}: {review_alternations}" ) assert set(review_alternations[0].split("|")) == set(REVIEW_FLAG_VALUES) + + +# --------------------------------------------------------------------------- +# EOL state vocabulary — Phase M +# --------------------------------------------------------------------------- + + +def test_eol_states_catalog_matches_schema_literals() -> None: + """The closed ``eol_state`` vocabulary lives in three places: the catalog + tuple (``services.eol.eol_catalog.EOL_STATES``, the values the evaluator + persists into ``component_versions.eol_state``), the ``ComponentSummary`` + Literal and the ``ComponentDetailResponse`` Literal (the wire contracts). + The FE mirror half is + ``apps/frontend/tests/unit/contracts/catalogMirrors.test.ts``. + """ + from services.eol.eol_catalog import EOL_STATES + from schemas.project_detail import ComponentDetailResponse, ComponentSummary + + expected = {"eol", "supported", "unknown"} + assert set(EOL_STATES) == expected + + def _literal_states(model: type, field: str) -> set[str]: + annotation = model.model_fields[field].annotation + # ``Literal["eol","supported","unknown"] | None`` — walk the union. + states: set[str] = set() + for arg in typing.get_args(annotation): + states.update(a for a in typing.get_args(arg) if isinstance(a, str)) + return states + + assert _literal_states(ComponentSummary, "eol_state") == expected + assert _literal_states(ComponentDetailResponse, "eol_state") == expected diff --git a/apps/frontend/src/features/projects/api/projectDetailApi.ts b/apps/frontend/src/features/projects/api/projectDetailApi.ts index 716df00d..0de31b91 100644 --- a/apps/frontend/src/features/projects/api/projectDetailApi.ts +++ b/apps/frontend/src/features/projects/api/projectDetailApi.ts @@ -65,6 +65,12 @@ export interface ProjectOverviewResponse { project_id: string; project_name: string; total_components: number; + /** + * Phase M — count of distinct components in the anchored scan past their + * published end-of-life (endoflife.date). Maintenance-risk KPI, distinct + * from CVEs. + */ + eol_count: number; /** Counts per severity bucket. May omit zero buckets — render with fallback. */ severity_distribution: Partial>; /** Counts per license category. */ @@ -139,6 +145,15 @@ export interface ComponentSummary { * ``optional`` so the strictest wins. */ dependency_scope: "required" | "optional" | null; + /** + * Phase M — endoflife.date verdict. ``eol`` = past published end-of-life, + * ``supported`` = tracked and supported, ``unknown`` = tracked but cycle + * undecidable, ``null`` = not a tracked product (closed whitelist). The + * EolBadge renders only for ``eol`` — absence is the signal. + */ + eol_state: EolState | null; + /** Published EOL date (ISO) when the feed is dated; else null. */ + eol_date: string | null; } export interface ComponentListResponse { @@ -212,12 +227,31 @@ export interface ComponentDetailResponse { * scan didn't encode a scope on the edge. */ dependency_scope: "required" | "optional" | null; + /** Phase M — endoflife.date verdict (see ComponentSummary.eol_state). */ + eol_state: EolState | null; + /** endoflife.date product slug the component mapped to, or null. */ + eol_product: string | null; + /** Release cycle derived from the version (e.g. "3.2"), or null. */ + eol_cycle: string | null; + /** Published EOL date (ISO) when the feed is dated; else null. */ + eol_date: string | null; + /** Snapshot provenance, e.g. "endoflife.date@2026-07-11". */ + eol_source: string | null; } // --------------------------------------------------------------------------- // Endpoints // --------------------------------------------------------------------------- +/** + * Phase M — the closed eol_state vocabulary. Mirrors + * ``services.eol.eol_catalog.EOL_STATES`` on the backend; contract-tested in + * tests/unit/contracts/catalogMirrors.test.ts against the backend half in + * apps/backend/tests/unit/test_catalog_contracts.py. + */ +export const EOL_STATES = ["eol", "supported", "unknown"] as const; +export type EolState = (typeof EOL_STATES)[number]; + export type ComponentSortKey = "name" | "severity" | "license"; export type SortOrder = "asc" | "desc"; @@ -248,6 +282,12 @@ export interface ListComponentsParams { * to include all buckets. */ dependency_scope?: DependencyScopeFilter[]; + /** + * Phase M — EOL facet. ``true`` keeps only components past their published + * end-of-life; ``false`` keeps everything else (including untracked). Omit + * / ``null`` to include both. + */ + eol?: boolean | null; /** * Pin the read to a specific succeeded scan (feature #28 snapshot anchoring). * Omit → the project's latest succeeded scan (unchanged default). An invalid / @@ -287,6 +327,10 @@ function listComponentsQuery( if (params.dependency_scope && params.dependency_scope.length > 0) { out.dependency_scope = params.dependency_scope; } + // Phase M — same emit-only-on-opinion contract as `direct`. + if (params.eol === true || params.eol === false) { + out.eol = params.eol; + } if (params.scanId != null && params.scanId.length > 0) { out.scan_id = params.scanId; } diff --git a/apps/frontend/src/features/projects/api/useComponents.ts b/apps/frontend/src/features/projects/api/useComponents.ts index 31bfbd71..173a333d 100644 --- a/apps/frontend/src/features/projects/api/useComponents.ts +++ b/apps/frontend/src/features/projects/api/useComponents.ts @@ -44,6 +44,11 @@ export interface ComponentsQueryFilters { * (``unspecified`` is the NULL-scope bucket). */ dependency_scope?: DependencyScopeFilter[]; + /** + * Phase M — EOL facet. ``true`` → only components past their published + * end-of-life; ``null``/``undefined`` → include both. + */ + eol?: boolean | null; /** * Pin the list to a specific succeeded scan (feature #28 snapshot anchoring). * `undefined` → latest succeeded scan. Part of the cache key so flipping the @@ -72,6 +77,7 @@ export function componentsKey( // W2 #31 — normalise to `null` so undefined ≡ null in the cache key. direct: filters.direct ?? null, dependency_scope: [...(filters.dependency_scope ?? [])].sort(), + eol: filters.eol ?? null, scanId: filters.scanId ?? null, }, ] as const; @@ -101,6 +107,7 @@ export function useComponents( filters.dependency_scope && filters.dependency_scope.length > 0 ? filters.dependency_scope : undefined, + eol: filters.eol ?? undefined, sort: filters.sort, order: filters.order, scanId: filters.scanId, diff --git a/apps/frontend/src/features/projects/components/ComponentDetailBody.tsx b/apps/frontend/src/features/projects/components/ComponentDetailBody.tsx index cf06fef4..bd4649b0 100644 --- a/apps/frontend/src/features/projects/components/ComponentDetailBody.tsx +++ b/apps/frontend/src/features/projects/components/ComponentDetailBody.tsx @@ -11,6 +11,7 @@ import type { } from "@/features/projects/api/projectDetailApi"; import { DependencyScopeBadge } from "@/features/projects/components/DependencyScopeBadge"; import { DependencyTypeBadge } from "@/features/projects/components/DependencyTypeBadge"; +import { EolBadge } from "@/features/projects/components/EolBadge"; import { LicenseCategoryBadge } from "@/features/projects/components/LicenseCategoryBadge"; import { SeverityBadge } from "@/features/projects/components/SeverityBadge"; import { @@ -129,6 +130,37 @@ export function ComponentDetailBody({ detail }: ComponentDetailBodyProps) { + {/* + * Phase M — end-of-life row. Always rendered (layout stability, + * same rationale as Type/Usage above): the EolBadge itself renders + * only for `eol`, so untracked / supported components show "—". + */} +
+ + {t("drawer.eol_label")} + + {detail.eol_state === "eol" ? ( + + ) : ( + + — + + )} +
( + () => searchParams.get("eol") === "true", + ); const [sort, setSort] = useState(() => parseSort(searchParams.get("sort")), ); @@ -301,6 +308,9 @@ export function ComponentsTab({ projectId, scanId }: ComponentsTabProps) { if (dependencyScope.length) next.set("dependency_scope", dependencyScope.join(",")); else next.delete("dependency_scope"); + // Phase M — `?eol=true` only when the toggle is on. + if (eolOnly) next.set("eol", "true"); + else next.delete("eol"); if (sort !== "name") next.set("sort", sort); else next.delete("sort"); if (order !== "asc") next.set("order", order); @@ -315,6 +325,7 @@ export function ComponentsTab({ projectId, scanId }: ComponentsTabProps) { licenseCategory, direct, dependencyScope, + eolOnly, sort, order, setSearchParams, @@ -327,6 +338,7 @@ export function ComponentsTab({ projectId, scanId }: ComponentsTabProps) { license_category: licenseCategory, direct, dependency_scope: dependencyScope, + eol: eolOnly ? true : null, sort, order, pageSize: PAGE_SIZE, @@ -338,6 +350,7 @@ export function ComponentsTab({ projectId, scanId }: ComponentsTabProps) { licenseCategory, direct, dependencyScope, + eolOnly, sort, order, scanId, @@ -504,6 +517,8 @@ export function ComponentsTab({ projectId, scanId }: ComponentsTabProps) { onDirectChange={setDirect} dependencyScope={dependencyScope} onDependencyScopeChange={setDependencyScope} + eolOnly={eolOnly} + onEolOnlyChange={setEolOnly} severity={severity} onSeverityChange={setSeverity} licenseCategory={licenseCategory} @@ -717,6 +732,11 @@ function ComponentsTableHeader({ {t("components.col.usage")} ) : null} + {visibleColumns.has("eol") ? ( + + {t("components.col.eol")} + + ) : null} {visibleColumns.has("severity") ? ( ) : null} + {visibleColumns.has("eol") ? ( + + + + ) : null} {visibleColumns.has("severity") ? ( diff --git a/apps/frontend/src/features/projects/components/ComponentsToolbar.tsx b/apps/frontend/src/features/projects/components/ComponentsToolbar.tsx index f2f8b515..f4f78ff8 100644 --- a/apps/frontend/src/features/projects/components/ComponentsToolbar.tsx +++ b/apps/frontend/src/features/projects/components/ComponentsToolbar.tsx @@ -101,6 +101,9 @@ export interface ComponentsToolbarProps { /** BD-style "Usage" multi-select. */ dependencyScope: DependencyScopeFilter[]; onDependencyScopeChange: (value: DependencyScopeFilter[]) => void; + /** Phase M — EOL-only toggle (`?eol=true` when on; off = no opinion). */ + eolOnly: boolean; + onEolOnlyChange: (value: boolean) => void; /** * W9 #52 — current severity selection (mirrors parent state). When the * user mounts the severity facet via "+ Add filter", the toolbar renders @@ -137,6 +140,8 @@ export function ComponentsToolbar({ onDirectChange, dependencyScope, onDependencyScopeChange, + eolOnly, + onEolOnlyChange, severity, onSeverityChange, licenseCategory, @@ -280,6 +285,32 @@ export function ComponentsToolbar({ /> + {/* Phase M — EOL-only toggle. A single pressed-state pill (not a + 3-state segment): "on" narrows to past-end-of-life components, + "off" means no opinion — `?eol=false` is never emitted from here. */} +
+ + {t("components.toolbar.eol_label")} + + +
+ {/* W9 #52 — optional severity facet, mount-on-demand. */} {showSeverityFacet ? (
+ + {t("components.eol.label")} + {showDate && eolDate ? ( + + {t("components.eol.since", { date: eolDate })} + + ) : null} + + ); +} diff --git a/apps/frontend/src/features/projects/components/OverviewTab.tsx b/apps/frontend/src/features/projects/components/OverviewTab.tsx index 0f52430e..effbfaeb 100644 --- a/apps/frontend/src/features/projects/components/OverviewTab.tsx +++ b/apps/frontend/src/features/projects/components/OverviewTab.tsx @@ -218,6 +218,45 @@ export function OverviewTab({ ) : null} + {/* Phase M — end-of-life KPI. Rendered only when the anchored scan + carries EOL components (absence is the signal, EolBadge contract); + clicking deep-links into the Components tab pre-filtered on + `?eol=true`. Amber (High family) — maintenance risk, not confirmed + exploitation. */} + {data.eol_count > 0 ? ( + + + + + {t("overview.eol_chip.title", { count: data.eol_count })} + + {t("overview.eol_chip.body")} + + + + ) : null} + {/* Row 1 left — Project info. Single column so it pairs with the gate card on the right; missing-`project` (standalone tests) keeps the card hidden and the gate card moves into the slot naturally. */} diff --git a/apps/frontend/src/locales/en/project_detail.json b/apps/frontend/src/locales/en/project_detail.json index 527a2ce2..cd1ce262 100644 --- a/apps/frontend/src/locales/en/project_detail.json +++ b/apps/frontend/src/locales/en/project_detail.json @@ -123,6 +123,11 @@ "none": "No risk" }, "overview": { + "eol_chip": { + "title": "{{count}} component(s) past end-of-life", + "body": "End-of-life releases receive no upstream fixes — a future Critical CVE will have no patch.", + "link": "View EOL components" + }, "info_card": { "title": "Project info", "description": "Description", @@ -233,9 +238,22 @@ "license": "License", "policy": "Policy", "usage": "Usage", + "eol": "EOL", "severity": "Severity", "vulns": "CVEs" }, + "eol": { + "label": "EOL", + "tooltip": "This release cycle is past its published end-of-life (endoflife.date) — no more upstream fixes.", + "tooltip_with_date": "End-of-life since {{date}} (endoflife.date) — no more upstream fixes.", + "since": "since {{date}}", + "state": { + "eol": "Past end-of-life", + "supported": "Supported (tracked by endoflife.date)", + "unknown": "Tracked product, but the release cycle could not be determined", + "untracked": "Not a product tracked by endoflife.date" + } + }, "license": { "unknown_dash": "—" }, @@ -269,7 +287,9 @@ "required": "Required", "optional": "Optional", "unspecified": "Unspecified" - } + }, + "eol_label": "End-of-life", + "eol_only": "EOL only" }, "empty": { "title": "No components match", @@ -306,6 +326,7 @@ "purl_label": "purl", "dependency_type_label": "Type", "usage_label": "Usage", + "eol_label": "End-of-life", "open_full_view": "Open in full view", "error": "Could not load component.", "vulns": { diff --git a/apps/frontend/src/locales/ko/project_detail.json b/apps/frontend/src/locales/ko/project_detail.json index 842cb2b0..d05c347b 100644 --- a/apps/frontend/src/locales/ko/project_detail.json +++ b/apps/frontend/src/locales/ko/project_detail.json @@ -123,6 +123,11 @@ "none": "리스크 없음" }, "overview": { + "eol_chip": { + "title": "지원 종료된 컴포넌트 {{count}}개", + "body": "지원 종료된 릴리즈에는 업스트림 수정이 제공되지 않아, 이후 발견되는 Critical CVE 에 패치가 없습니다.", + "link": "EOL 컴포넌트 보기" + }, "info_card": { "title": "프로젝트 정보", "description": "설명", @@ -233,9 +238,22 @@ "license": "라이선스", "policy": "정책", "usage": "용도", + "eol": "EOL", "severity": "심각도", "vulns": "CVE" }, + "eol": { + "label": "EOL", + "tooltip": "이 릴리즈 사이클은 공표된 지원 종료(endoflife.date)를 지났습니다 — 업스트림 수정이 더 이상 제공되지 않습니다.", + "tooltip_with_date": "{{date}}부터 지원 종료(endoflife.date) — 업스트림 수정이 더 이상 제공되지 않습니다.", + "since": "{{date}}부터", + "state": { + "eol": "지원 종료됨", + "supported": "지원 중 (endoflife.date 추적 대상)", + "unknown": "추적 대상이나 릴리즈 사이클을 판정할 수 없음", + "untracked": "endoflife.date 추적 대상이 아님" + } + }, "license": { "unknown_dash": "—" }, @@ -269,7 +287,9 @@ "required": "필수", "optional": "선택", "unspecified": "지정 안 됨" - } + }, + "eol_label": "지원 종료", + "eol_only": "EOL만" }, "empty": { "title": "조건에 맞는 컴포넌트가 없습니다", @@ -306,6 +326,7 @@ "purl_label": "purl", "dependency_type_label": "유형", "usage_label": "용도", + "eol_label": "지원 종료", "open_full_view": "전체 화면으로 보기", "error": "컴포넌트를 불러오지 못했습니다.", "vulns": { diff --git a/apps/frontend/tests/e2e/components_eol.spec.ts b/apps/frontend/tests/e2e/components_eol.spec.ts new file mode 100644 index 00000000..5003e0d2 --- /dev/null +++ b/apps/frontend/tests/e2e/components_eol.spec.ts @@ -0,0 +1,138 @@ +/** + * Components EOL E2E — Phase M (endoflife.date end-of-life flagging). + * + * Structural sibling of `vulnerabilities_kev.spec.ts` (sharedPage + seed-skip + * conventions). The seed stamps EOL columns on exactly the FIRST seeded + * component (`-00000`: eol_state='eol', eol_date=2020-01-01 — a + * far-past date that can never flip with the run date); every other + * component stays NULL (untracked). Scenarios: + * + * E1 — Exactly one row carries the EOL badge; absence is the signal for + * every untracked row (the EolBadge renders nothing for NULL / + * supported / unknown — the KevBadge contract). + * E2 — The toolbar's "EOL only" toggle narrows the list to the flagged + * row and mirrors `?eol=true` into the URL; toggling off restores + * the full list and drops the param. + * E3 — The flagged component's drawer shows the EOL row with the badge + + * inline date; an untracked component's drawer shows the "—" row. + * E4 — The Overview tab surfaces the EOL KPI chip with data-eol-count=1; + * its link deep-links into the Components tab pre-filtered. + * + * Selectors anchor on data-testid / data-eol-state / data-eol-count — never + * translated copy — so the suite passes on EN and KO alike. + */ +import { type Page, expect, test } from "@playwright/test"; + +import { AuthHarness } from "../_harness/auth"; +import { PortalPage } from "../_harness/PortalPage"; +import { seedE2eUser, type SeedSummary } from "../_harness/seed"; + +const PROJECT_NAME = "ci-eol"; +const COMPONENT_COUNT = 6; +const PREFIX = `eolseed${Date.now().toString(36)}`; // avoid cross-run purl collisions +const EOL_COMPONENT = `${PREFIX}-00000`; +const UNTRACKED_COMPONENT = `${PREFIX}-00001`; + +let sharedPage: Page; +let seedFailed = false; +let seed: SeedSummary; + +test.describe.serial("@components end-of-life badge + filter", () => { + test.beforeAll(async ({ browser }) => { + sharedPage = await browser.newPage(); + try { + seed = seedE2eUser({ + projectNames: [PROJECT_NAME], + withScan: true, + componentCount: COMPONENT_COUNT, + componentPrefix: PREFIX, + }); + } catch { + seedFailed = true; + return; + } + const auth = new AuthHarness(sharedPage); + await auth.clearAuthState(); + await auth.gotoLogin(); + await auth.login(seed.email, seed.password); + }); + + test.afterAll(async () => { + await sharedPage?.close(); + }); + + test.beforeEach(async () => { + test.skip( + seedFailed, + "seed precondition failed — bring docker-compose dev up + ensure python3 is on PATH", + ); + const portal = new PortalPage(sharedPage); + await portal.gotoProjects(); + await portal.openProjectDetail(PROJECT_NAME); + await portal.selectTab("components"); + }); + + test("E1) exactly the seeded EOL row carries the badge", async () => { + // All rows land in one virtualization window at COMPONENT_COUNT=6. + await expect( + sharedPage.getByTestId("component-row-cell-eol").first(), + ).toBeVisible(); + const badges = sharedPage.getByTestId("eol-badge"); + await expect(badges).toHaveCount(1); + await expect(badges.first()).toHaveAttribute("data-eol-state", "eol"); + await expect(badges.first()).toHaveAttribute( + "data-eol-date", + "2020-01-01", + ); + }); + + test("E2) the EOL-only toggle narrows the list and mirrors ?eol=true", async () => { + const toggle = sharedPage.getByTestId("components-eol-filter"); + await toggle.click(); + await expect(toggle).toHaveAttribute("data-active", "true"); + await expect(sharedPage).toHaveURL(/eol=true/); + + const summary = sharedPage.getByTestId("components-summary"); + await expect(summary).toHaveAttribute("data-total", "1"); + await expect( + sharedPage.getByText(EOL_COMPONENT, { exact: true }), + ).toBeVisible(); + + await toggle.click(); + await expect(toggle).toHaveAttribute("data-active", "false"); + await expect(sharedPage).not.toHaveURL(/eol=true/); + await expect(summary).toHaveAttribute("data-total", String(COMPONENT_COUNT)); + }); + + test("E3) drawer surfaces the EOL row (badge + date) and the untracked dash", async () => { + const portal = new PortalPage(sharedPage); + await portal.openComponentDrawer(EOL_COMPONENT); + const eolRow = sharedPage.getByTestId("component-drawer-eol"); + await expect(eolRow).toBeVisible(); + await expect(eolRow.getByTestId("eol-badge")).toBeVisible(); + await expect(eolRow.getByTestId("eol-badge-date")).toBeVisible(); + await sharedPage.keyboard.press("Escape"); + + await portal.openComponentDrawer(UNTRACKED_COMPONENT); + const untrackedRow = sharedPage.getByTestId("component-drawer-eol"); + await expect(untrackedRow).toBeVisible(); + await expect(untrackedRow.getByTestId("eol-badge")).toHaveCount(0); + await expect(untrackedRow).toContainText("—"); + await sharedPage.keyboard.press("Escape"); + }); + + test("E4) overview KPI chip counts the EOL component and deep-links", async () => { + const portal = new PortalPage(sharedPage); + await portal.selectTab("overview"); + const chip = sharedPage.getByTestId("overview-eol-chip"); + await expect(chip).toBeVisible(); + await expect(chip).toHaveAttribute("data-eol-count", "1"); + + await sharedPage.getByTestId("overview-eol-chip-link").click(); + await expect(sharedPage).toHaveURL(/tab=components/); + await expect(sharedPage).toHaveURL(/eol=true/); + await expect( + sharedPage.getByTestId("components-eol-filter"), + ).toHaveAttribute("data-active", "true"); + }); +}); diff --git a/apps/frontend/tests/unit/components/ComponentDetailBody.test.tsx b/apps/frontend/tests/unit/components/ComponentDetailBody.test.tsx index 7be4bd9e..cc74b88d 100644 --- a/apps/frontend/tests/unit/components/ComponentDetailBody.test.tsx +++ b/apps/frontend/tests/unit/components/ComponentDetailBody.test.tsx @@ -45,6 +45,11 @@ function detail( depth: null, direct: false, dependency_scope: null, + eol_state: null, + eol_product: null, + eol_cycle: null, + eol_date: null, + eol_source: null, ...overrides, }; } diff --git a/apps/frontend/tests/unit/contracts/catalogMirrors.test.ts b/apps/frontend/tests/unit/contracts/catalogMirrors.test.ts index 089398d9..72412401 100644 --- a/apps/frontend/tests/unit/contracts/catalogMirrors.test.ts +++ b/apps/frontend/tests/unit/contracts/catalogMirrors.test.ts @@ -32,6 +32,7 @@ import { describe, expect, it } from "vitest"; import { NOTIFICATION_KINDS } from "@/features/notifications/api/notificationsApi"; import { G7_CLUSTER_ORDER } from "@/features/scan/lib/g7Conformance"; import { KNOWN_OBLIGATION_KINDS } from "@/features/projects/api/obligationsApi"; +import { EOL_STATES } from "@/features/projects/api/projectDetailApi"; import { REVIEW_FLAG_VALUES } from "@/features/projects/api/licensesApi"; import { ALL_VULNERABILITY_STATUSES } from "@/features/projects/lib/vulnerabilityTransitions"; import { visualFor } from "@/features/projects/components/ProjectStatusBadge"; @@ -347,3 +348,29 @@ describe("vulnerability statuses — label-map half of the 7-state mirror", () = }, ); }); + +describe("EOL states — FE mirror of services.eol.eol_catalog.EOL_STATES", () => { + // Phase M — the closed eol_state vocabulary lives twice: the backend + // catalog (services/eol/eol_catalog.py EOL_STATES, persisted into + // component_versions.eol_state) and the FE mirror EOL_STATES used by the + // EolBadge / drawer labels. The backend half of this contract is + // apps/backend/tests/unit/test_catalog_contracts.py. + it("matches the backend's closed eol_state set, in canonical order", () => { + expect([...EOL_STATES]).toEqual(["eol", "supported", "unknown"]); + }); + + it.each([ + ["en", enProjectDetail], + ["ko", koProjectDetail], + ])( + "every eol state owns a %s `components.eol.state.*` label (plus untracked)", + (_locale, ns) => { + const states = labelMap(ns, "components", "eol", "state"); + for (const state of EOL_STATES) { + expect(states[state], `components.eol.state.${state} missing`).toBeTruthy(); + } + // The NULL (not-a-tracked-product) bucket renders through `untracked`. + expect(states.untracked, "components.eol.state.untracked missing").toBeTruthy(); + }, + ); +}); diff --git a/apps/frontend/tests/unit/features/projects/ComponentDrawer.test.tsx b/apps/frontend/tests/unit/features/projects/ComponentDrawer.test.tsx index e1a622a2..41c342a4 100644 --- a/apps/frontend/tests/unit/features/projects/ComponentDrawer.test.tsx +++ b/apps/frontend/tests/unit/features/projects/ComponentDrawer.test.tsx @@ -51,6 +51,11 @@ function detail( depth: null, direct: false, dependency_scope: null, + eol_state: null, + eol_product: null, + eol_cycle: null, + eol_date: null, + eol_source: null, ...overrides, }; } diff --git a/apps/frontend/tests/unit/features/projects/ComponentsTab.test.tsx b/apps/frontend/tests/unit/features/projects/ComponentsTab.test.tsx index 214b0887..65409966 100644 --- a/apps/frontend/tests/unit/features/projects/ComponentsTab.test.tsx +++ b/apps/frontend/tests/unit/features/projects/ComponentsTab.test.tsx @@ -74,6 +74,8 @@ function comp( depth: null, direct: false, dependency_scope: null, + eol_state: null, + eol_date: null, ...overrides, }; } @@ -108,6 +110,11 @@ function detail( depth: null, direct: false, dependency_scope: null, + eol_state: null, + eol_product: null, + eol_cycle: null, + eol_date: null, + eol_source: null, ...overrides, }; } diff --git a/apps/frontend/tests/unit/features/projects/OverviewTab.test.tsx b/apps/frontend/tests/unit/features/projects/OverviewTab.test.tsx index 7d75cd29..38485618 100644 --- a/apps/frontend/tests/unit/features/projects/OverviewTab.test.tsx +++ b/apps/frontend/tests/unit/features/projects/OverviewTab.test.tsx @@ -35,6 +35,7 @@ function overview( project_id: "11111111-1111-1111-1111-111111111111", project_name: "demo", total_components: 12, + eol_count: 0, severity_distribution: { critical: 1, high: 2, medium: 3, low: 6 }, license_distribution: { forbidden: 1, allowed: 11 }, risk_score: 80, diff --git a/apps/frontend/tests/unit/features/projects/ProjectDetailPage.test.tsx b/apps/frontend/tests/unit/features/projects/ProjectDetailPage.test.tsx index 67f038da..659e15d0 100644 --- a/apps/frontend/tests/unit/features/projects/ProjectDetailPage.test.tsx +++ b/apps/frontend/tests/unit/features/projects/ProjectDetailPage.test.tsx @@ -135,6 +135,7 @@ function overview( project_id: "proj-1", project_name: "Demo Project", total_components: 5, + eol_count: 0, severity_distribution: { critical: 1, high: 1 }, license_distribution: { allowed: 4, forbidden: 1 }, risk_score: 80, diff --git a/apps/frontend/tests/unit/pages/ComponentDetailPage.test.tsx b/apps/frontend/tests/unit/pages/ComponentDetailPage.test.tsx index 23e883e3..4267a062 100644 --- a/apps/frontend/tests/unit/pages/ComponentDetailPage.test.tsx +++ b/apps/frontend/tests/unit/pages/ComponentDetailPage.test.tsx @@ -95,6 +95,11 @@ function makeComponent( depth: null, direct: false, dependency_scope: null, + eol_state: null, + eol_product: null, + eol_cycle: null, + eol_date: null, + eol_source: null, ...overrides, }; } diff --git a/docs-site/docs/reference/data-sources.md b/docs-site/docs/reference/data-sources.md index 2022c56d..35f9a5f2 100644 --- a/docs-site/docs/reference/data-sources.md +++ b/docs-site/docs/reference/data-sources.md @@ -26,6 +26,12 @@ Security leads, auditors, and operators answering "where does this CVE come from All five live in the same Trivy DB bundle — the portal does **not** call any of these APIs at scan time. Per-scan matching reads from `/var/lib/trivy/db/` on the worker, which the [Trivy DB refresh task](../admin-guide/vulnerability-data.md) keeps current. +One component-level source sits outside the Trivy DB: + +| Source | Publisher | Refresh | What it contributes | +|---|---|---|---| +| **endoflife.date** — product lifecycle dates | endoflife.date community (code MIT; lifecycle dates are factual data) | Snapshot vendored per release (`scripts/refresh_eol_snapshot.py`) | End-of-life verdicts for a curated whitelist of runtimes/frameworks — the [EOL badge and filter](../user-guide/components-and-licenses.md#end-of-life-flagging). Read from the bundled snapshot at scan time; zero network calls. | + ## Refresh cadence in the portal | Layer | Cadence | Knob | diff --git a/docs-site/docs/reference/env-variables.md b/docs-site/docs/reference/env-variables.md index dbe42bb4..ae09d3af 100644 --- a/docs-site/docs/reference/env-variables.md +++ b/docs-site/docs/reference/env-variables.md @@ -122,6 +122,8 @@ See [build gate](./glossary.md#build-gates) for the gate model and [Gate the bui | `SCAN_SCOPE_FILTER_ENABLED` | `true` | `config.py` | Master switch for the runtime-scope post-filter: source scans drop non-deployable dependencies (Maven `test`/`provided`, npm `devDependencies`) from the SBOM before persist, signing and Trivy matching. Pure local transformation, no egress. Only the exact tokens `false` / `0` / `no` disable it. See [Components & licenses → Runtime-scope filtering](../user-guide/components-and-licenses.md#runtime-scope-filtering). | | `SCAN_SCOPE_FILTER_MAVEN_ENABLED` | `true` | `config.py` | Maven half of the scope filter (drops cdxgen scope `optional`/`excluded` nodes). Disable if a project relies on Maven `true` **runtime** dependencies — cdxgen tags those `optional` exactly like test scope, so they are dropped too. | | `SCAN_SCOPE_FILTER_NODE_ENABLED` | `true` | `config.py` | npm half of the scope filter (drops packages the committed/generated `package-lock.json` classifies as `dev`). A package absent from the lockfile is always kept. | +| `EOL_ENABLED` | `true` | `config.py` | End-of-life flagging: components matching the endoflife.date product whitelist are stamped `eol` / `supported` / `unknown` on the shared catalog. Fully offline — verdicts come from a snapshot vendored with the release, zero egress. Only `false` / `0` / `no` disable. See [Components & licenses → End-of-life flagging](../user-guide/components-and-licenses.md#end-of-life-flagging). | +| `EOL_SNAPSHOT_PATH` | *(empty — vendored file)* | `config.py` | Operator override for the endoflife.date snapshot. Air-gapped installs can build a fresher snapshot on a connected host (`python3 scripts/refresh_eol_snapshot.py`), mount it, and point this at the file. | | `WORKSPACE_HOST_PATH` | `/tmp/trustedoss` | `config.py`, `docker-compose.yml` | Host directory mounted into the worker as `/workspace`. Holds repo clones + scan artefacts (cdxgen SBOM, scancode output). The compose stack overrides this to `/workspace` inside the container. | | `ORT_RULES_PATH` | `/opt/trustedoss/ort/rules.kts` | `docker-compose.yml` | Legacy path inside the worker, vestigial after the ORT stage was removed. The file is a placeholder and has no effect in this release — license-tier classification comes from `_LICENSE_CATEGORY_DEFAULTS` in `apps/backend/tasks/scan_source.py`. | | `JSONB_ROW_SIZE_LIMIT_BYTES` | `262144` (256 KB) | `config.py` | Per-row JSON byte ceiling before the writer truncates and emits a warning. Guards the I-1 unbounded-payload class. | diff --git a/docs-site/docs/user-guide/components-and-licenses.md b/docs-site/docs/user-guide/components-and-licenses.md index 1668b4aa..3f985a6b 100644 --- a/docs-site/docs/user-guide/components-and-licenses.md +++ b/docs-site/docs/user-guide/components-and-licenses.md @@ -25,6 +25,7 @@ Columns: - **Version** — pinned version found in the manifest or lockfile. - **License** — the license attached to the component. For a dependency this is the **declared** license `cdxgen` read from package metadata; see [Declared vs. detected](#declared-vs-detected) for how detected and concluded licenses relate. This is the value used by the build gate. - **Usage** — **Required** / **Optional** / `—`. The dependency scope `cdxgen` recorded along the *shortest* path to this component (when the same component is reachable via several paths the highest-scope path wins, i.e. `Required` > `Optional`). `—` means the scanner did not emit a scope for this component. Optional dependencies often carry the same legal obligations as required ones, but the **Required** / **Optional** distinction maps to license-compliance burden — an unused `Optional` extra is cheaper to remove than a deeply-required transitive dependency. +- **EOL** — an **EOL** badge when the component's release cycle is past its published end-of-life; blank otherwise. See [End-of-life flagging](#end-of-life-flagging). - **Severity** — the highest severity across this component's open CVEs (carries the license-classification color via the legend). - **CVEs** — count of open vulnerabilities for this component (clickable; jumps to the Vulnerabilities tab pre-filtered). @@ -39,6 +40,7 @@ The inline filter bar at the top supports: - **Usage** — multi-select (`Required` / `Optional`). Selecting both is equivalent to no filter; selecting only an unknown value drops to an empty page rather than 422-rejecting (consistent with severity / license-category filter semantics). - **Severity** — multi-select badges (Critical / High / Medium / Low / Info). - **License category** — multi-select (`Allowed` / `Conditional` / `Forbidden` / `Unknown`). +- **EOL only** — a toggle that narrows the table to components past their end-of-life (`?eol=true`). - **Sort** + **order** — column-driven sort with ascending / descending toggle. Filters compose. The URL updates (`?direct=…`, `?dependency_scope=…`, …) so you can share a filtered view. @@ -75,6 +77,37 @@ Two caveats worth knowing: resolved graph on the next scan. See [Environment variables](../reference/env-variables.md) for all three toggles. +### End-of-life flagging {#end-of-life-flagging} + +A runtime or framework past its published **end-of-life (EOL)** receives no +upstream fixes — a Critical CVE found tomorrow will have no patch. That is a +supply-chain risk distinct from today's CVE count, so the scanner flags it +separately: every scanned component is matched against a curated whitelist of +products tracked by [endoflife.date](https://endoflife.date) (Spring Boot, +Spring Framework, Express, Next.js, Angular, Vue, Django, Rails, Symfony, +Laravel), its release cycle is derived from the version (`3.2.0` → cycle +`3.2`), and the cycle's published EOL is read from a snapshot **bundled with +the release** — scans make zero network calls, so this works air-gapped. + +How to read the surfaces: + +- **EOL column / badge** — the amber **EOL** badge appears only when the + cycle is *past* end-of-life. The published date rides the badge tooltip. +- **Blank vs. unknown** — a blank cell means the component is **not a + tracked product** (the whitelist is deliberately closed: long-tail + libraries are absent and the scanner never guesses). The drawer's + End-of-life row distinguishes further: `—` with a tooltip saying either + "not tracked" or "tracked, but the release cycle could not be determined". +- **Overview chip** — when the anchored scan contains EOL components the + Overview tab shows a count with a one-click jump to the filtered list. +- The verdict is stored on the shared component catalog and re-evaluated + when a newer snapshot arrives (release upgrades), so older projects pick + up newly-published EOL dates without a re-scan. + +Set `EOL_ENABLED=false` to turn the flagging off, or point +`EOL_SNAPSHOT_PATH` at a fresher snapshot file on air-gapped installs — see +[Environment variables](../reference/env-variables.md). + ## Table view vs. graph view The Components tab has a **Table / Graph** toggle (top-left). **Table** is the default virtualized list above. **Graph** renders the scan's resolved **dependency graph** — every *parent → child* edge the scanner recorded — as an interactive node-link diagram (left-to-right layout), so you can see *how* a package is pulled in rather than just *that* it is present. Each node is coloured by its highest-severity finding (colour is never the only signal — the detail panel and the tree fallback show a severity label too), and a search box highlights matching packages. Clicking a node opens its details beside the canvas. diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/data-sources.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/data-sources.md index ae0df614..a4979764 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/data-sources.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/data-sources.md @@ -26,6 +26,12 @@ TRUSCA는 5개 공개 취약점 피드를 컴파일한 번들인 **Trivy DB**로 다섯 출처 모두 동일한 Trivy DB 번들에 함께 들어 있습니다 — 포털은 스캔 시점에 이 API들을 **호출하지 않습니다**. 스캔별 매칭은 워커의 `/var/lib/trivy/db/`에서 읽으며, [Trivy DB refresh 태스크](../admin-guide/vulnerability-data.md)가 이를 최신으로 유지합니다. +컴포넌트 수준 출처 하나는 Trivy DB 밖에 있습니다: + +| 출처 | 발행처 | 갱신 | 기여 내용 | +|---|---|---|---| +| **endoflife.date** — 제품 라이프사이클 날짜 | endoflife.date 커뮤니티 (코드는 MIT, 라이프사이클 날짜는 사실 데이터) | 릴리즈마다 벤더링되는 스냅숏 (`scripts/refresh_eol_snapshot.py`) | 선별된 런타임·프레임워크 목록의 지원 종료 판정 — [EOL 배지와 필터](../user-guide/components-and-licenses.md#end-of-life-flagging). 스캔 시 번들된 스냅숏에서 읽으며 네트워크 호출이 없습니다. | + ## 포털에서의 갱신 주기 | 레이어 | 주기 | 노브 | diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md index 60b7e1b5..65825d0f 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md @@ -121,6 +121,8 @@ CI 빌드 게이트는 기본적으로 Critical CVE와 금지 라이선스에서 | `SCAN_SCOPE_FILTER_ENABLED` | `true` | `config.py` | 런타임 스코프 필터의 마스터 스위치: 소스 스캔이 저장·서명·Trivy 매칭 전에 배포되지 않는 의존성(Maven `test`/`provided`, npm `devDependencies`)을 SBOM 에서 제거합니다. 외부 전송 없는 순수 로컬 변환입니다. 정확히 `false` / `0` / `no` 토큰만 끕니다. [컴포넌트·라이선스 → 런타임 스코프 필터링](../user-guide/components-and-licenses.md#runtime-scope-filtering) 참고. | | `SCAN_SCOPE_FILTER_MAVEN_ENABLED` | `true` | `config.py` | 스코프 필터의 Maven 부분(cdxgen scope `optional`/`excluded` 노드 제거). 프로젝트가 Maven `true` **런타임** 의존성을 쓰면 끄십시오 — cdxgen 이 test scope 와 똑같이 `optional` 로 태깅해 함께 제거됩니다. | | `SCAN_SCOPE_FILTER_NODE_ENABLED` | `true` | `config.py` | 스코프 필터의 npm 부분(커밋되었거나 prep 단계가 생성한 `package-lock.json` 이 `dev` 로 분류한 패키지 제거). lockfile 에 없는 패키지는 항상 유지합니다. | +| `EOL_ENABLED` | `true` | `config.py` | 지원 종료(EOL) 표시: endoflife.date 추적 제품 목록에 맞는 컴포넌트를 공유 카탈로그에 `eol` / `supported` / `unknown` 으로 기록합니다. 완전 오프라인 — 판정은 릴리즈에 벤더링된 스냅숏에서 나오며 외부 전송이 없습니다. 정확히 `false` / `0` / `no` 만 끕니다. [컴포넌트·라이선스 → 지원 종료 표시](../user-guide/components-and-licenses.md#end-of-life-flagging) 참고. | +| `EOL_SNAPSHOT_PATH` | *(빈 값 — 벤더 파일)* | `config.py` | endoflife.date 스냅숏 재정의. air-gapped 설치에서는 연결된 호스트에서 더 신선한 스냅숏을 만들어(`python3 scripts/refresh_eol_snapshot.py`) 마운트한 뒤 이 변수로 지정합니다. | | `WORKSPACE_HOST_PATH` | `/tmp/trustedoss` | `config.py`, `docker-compose.yml` | worker에 `/workspace`로 마운트되는 호스트 디렉터리. 레포 클론 + 스캔 아티팩트(cdxgen SBOM, scancode 출력) 보관. compose 스택은 컨테이너 내에서 `/workspace`로 오버라이드합니다. | | `ORT_RULES_PATH` | `/opt/trustedoss/ort/rules.kts` | `docker-compose.yml` | worker 내부 레거시 경로로, ORT 단계 제거 후 잔재입니다. 파일은 placeholder 이며 v0.10.0 에서는 효과가 없습니다 — 라이선스 단계 분류는 `apps/backend/tasks/scan_source.py` 의 `_LICENSE_CATEGORY_DEFAULTS` 에서 옵니다. | | `JSONB_ROW_SIZE_LIMIT_BYTES` | `262144` (256 KB) | `config.py` | writer가 truncate + warn하기 전 행당 JSON 바이트 상한. I-1 무한 페이로드 클래스 가드. | diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md index d6ab79a3..30690988 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/user-guide/components-and-licenses.md @@ -25,6 +25,7 @@ sidebar_position: 3 - **버전 (Version)** — 매니페스트나 락파일에서 고정된 버전. - **라이선스 (License)** — 컴포넌트에 부여된 라이선스. 의존성의 경우 `cdxgen` 이 패키지 메타데이터에서 읽은 **declared** 라이선스입니다. detected·concluded 와의 관계는 [declared vs. detected](#declared-vs-detected) 참고. 빌드 게이트가 사용하는 값입니다. - **Usage** — **Required** / **Optional** / `—`. `cdxgen` 이 본 컴포넌트로 가는 *가장 얕은* 경로에서 기록한 의존성 스코프(같은 컴포넌트가 여러 경로로 도달 가능하면 가장 높은 스코프 — `Required` > `Optional` — 가 이김). `—` 는 스캐너가 스코프를 emit 하지 않은 경우. Optional 의존성은 Required 와 같은 법적 의무를 가지는 경우가 많지만, **Required / Optional** 구분은 라이선스 컴플라이언스 부담에 매핑됩니다 — 사용하지 않는 `Optional` extra 는 깊이 박힌 required transitive 의존성보다 제거 비용이 낮습니다. +- **EOL** — 컴포넌트의 릴리즈 사이클이 공표된 지원 종료를 지났으면 **EOL** 배지, 아니면 빈칸. [지원 종료 표시](#end-of-life-flagging) 참고. - **심각도 (Severity)** — 본 컴포넌트의 미해결 CVE 중 가장 높은 심각도(범례를 통해 라이선스 분류 색상도 함께 표시). - **CVEs** — 본 컴포넌트의 미해결 취약점 수(클릭 시 사전 필터링된 Vulnerabilities 탭으로 이동). @@ -39,6 +40,7 @@ sidebar_position: 3 - **Usage** — 다중 선택(`Required` / `Optional`). 둘 다 선택하면 필터 없음과 동일합니다. 미지값만 선택한 쿼리는 422 거부 대신 empty page 로 드롭됩니다(심각도·라이선스 카테고리 필터 시멘틱과 일치). - **심각도 (Severity)** — 다중 선택 배지(Critical / High / Medium / Low / Info). - **라이선스 카테고리 (License category)** — 다중 선택(`Allowed` / `Conditional` / `Forbidden` / `Unknown`). +- **EOL만 (EOL only)** — 지원 종료된 컴포넌트만 남기는 토글(`?eol=true`). - **정렬 (Sort)** + **순서 (order)** — 컬럼 기반 정렬과 오름·내림 토글. 필터는 결합됩니다. URL(`?direct=…`, `?dependency_scope=…`, …)이 갱신되어 필터된 뷰를 공유할 수 있습니다. @@ -74,6 +76,37 @@ cdxgen 은 해석된 모든 노드를 기록하므로, 산출물과 함께 배 그래프가 복원됩니다. 토글 3종은 [환경변수](../reference/env-variables.md)를 참고하십시오. +### 지원 종료(EOL) 표시 {#end-of-life-flagging} + +공표된 **지원 종료(EOL, end-of-life)** 를 지난 런타임·프레임워크에는 +업스트림 수정이 더 이상 제공되지 않습니다 — 내일 발견되는 Critical CVE 에 +패치가 없다는 뜻입니다. 오늘의 CVE 수와는 별개의 공급망 리스크라서 스캐너가 +따로 표시합니다. 스캔된 모든 컴포넌트를 [endoflife.date](https://endoflife.date) +추적 대상 제품의 선별 목록(Spring Boot, Spring Framework, Express, Next.js, +Angular, Vue, Django, Rails, Symfony, Laravel)과 대조하고, 버전에서 릴리즈 +사이클을 유도한 뒤(`3.2.0` → 사이클 `3.2`), 해당 사이클의 지원 종료일을 +**릴리즈에 번들된 스냅숏**에서 읽습니다 — 스캔 시 네트워크 호출이 없어 +air-gapped 환경에서도 동작합니다. + +화면 읽는 방법: + +- **EOL 컬럼/배지** — 사이클이 지원 종료를 *지난* 경우에만 주황색 **EOL** + 배지가 나타납니다. 공표된 종료일은 배지 툴팁에 표시됩니다. +- **빈칸과 unknown 의 구분** — 빈칸은 **추적 대상 제품이 아니라는** 뜻입니다 + (목록은 의도적으로 닫혀 있습니다: 롱테일 라이브러리는 없고, 스캐너는 절대 + 추정하지 않습니다). 드로어의 지원 종료 행은 한 단계 더 구분합니다: `—` 에 + 마우스를 올리면 "추적 대상 아님" 또는 "추적 대상이나 릴리즈 사이클을 + 판정할 수 없음"이 표시됩니다. +- **Overview 칩** — 기준 스캔에 EOL 컴포넌트가 있으면 Overview 탭에 개수와 + 함께 필터된 목록으로 바로 이동하는 링크가 나타납니다. +- 판정은 공유 컴포넌트 카탈로그에 저장되고 새 스냅숏이 도착하면(릴리즈 + 업그레이드) 재평가되므로, 오래된 프로젝트도 재스캔 없이 새로 공표된 + 지원 종료일을 반영합니다. + +`EOL_ENABLED=false` 로 끄거나, air-gapped 설치에서는 `EOL_SNAPSHOT_PATH` 로 +더 신선한 스냅숏 파일을 지정할 수 있습니다 — +[환경변수](../reference/env-variables.md) 참고. + ## 표 보기와 그래프 보기 Components 탭에는 **표 / 그래프** 토글이 있습니다(좌측 상단). 기본값은 위의 가상 스크롤 목록(**표**)입니다. **그래프**는 스캔이 해석한 **의존성 그래프**(스캐너가 기록한 모든 부모 → 자식 엣지)를 상호작용형 노드-링크 다이어그램(좌→우 배치)으로 렌더링합니다. 어떤 패키지가 단지 존재하는지가 아니라 어떻게 끌려 들어왔는지를 볼 수 있습니다. 각 노드는 가장 높은 심각도 결과에 따라 색으로 표시되며(색은 유일한 신호가 아닙니다 — 상세 패널과 트리 폴백에 심각도 라벨도 함께 표시), 검색 상자로 일치하는 패키지를 강조할 수 있습니다. 노드를 클릭하면 캔버스 옆에 상세가 열립니다. From 08973ef024b3fddcbb94b5987d1ab4488671e12f Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 01:39:36 +0900 Subject: [PATCH 06/10] feat(ops): weekly EOL re-stamp beat + admin snapshot health panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close out the EOL feature's ops layer (KEV subsystem structural mirror): - eol_sync_state (0039): the kev_sync_state single-row pattern plus one addition — the fetched dataset itself rides the row (JSONB, a few KB) so the re-stamp pass can prefer the freshest data without network. - tasks/eol_catalog_refresh: two-half tick. The fetch half is env-gated (EOL_REFRESH_ENABLED, DEFAULT OFF — new third-party egress follows the SCANOSS fail-closed posture, not the KEV one; EOL dates churn quarterly and the per-release vendored snapshot bounds staleness) with a proportional sanity floor (at least half the mapped products) so a gutted sweep never displaces a good dataset. The re-stamp half ALWAYS runs — pure local: effective dataset = newer of (vendored, fetched), bounded whitelist-prefix SELECT, changed-value-guarded stamps, and a clear pass for rows the whitelist no longer covers (the per-scan hook enriches but never reconciles). Weekly beat Sunday 02:15 UTC, between the KEV and Trivy ticks. - integrations/eol_feed: kev_feed discipline scaled to many small documents — per-product byte ceiling + skip-and-count, 60s whole-run budget, EolFeedUnavailable only when NOTHING fetched, host-only logging. - Admin surface: GET /v1/admin/eol/health (super-admin, existence-hide) assembled by eol_health_service (kev_health mirror + effective-snapshot resolution); FE EolPanel with a 180-day stale escalation, stamped/cleared KPIs and an origin/refresh footer; EN/KO keys; panel unit tests. - Docs EN/KO: admin health guide section (docs-uat marker on the endpoint), EOL_REFRESH_* env rows. OpenAPI snapshot regenerated. - Also: mypy-strict cleanups in the Phase K/L test files surfaced by the full 'mypy .' run. --- CHANGELOG.md | 9 + .../alembic/versions/0039_eol_sync_state.py | 94 +++++ apps/backend/api/v1/admin/__init__.py | 4 +- apps/backend/api/v1/admin/eol.py | 48 +++ apps/backend/core/config.py | 45 +++ apps/backend/integrations/eol_feed.py | 220 +++++++++++ apps/backend/models/__init__.py | 4 + apps/backend/models/eol_sync_state.py | 80 ++++ apps/backend/schemas/admin_ops.py | 116 +++++- apps/backend/services/eol_health_service.py | 169 ++++++++ apps/backend/tasks/celery_app.py | 20 + apps/backend/tasks/eol_catalog_refresh.py | 367 ++++++++++++++++++ .../test_cdxgen_cocoapods_exclude.py | 4 +- .../integrations/test_cocoapods_lockfile.py | 5 +- .../tests/unit/integrations/test_eol_feed.py | 136 +++++++ .../backend/tests/unit/openapi_endpoints.json | 1 + .../unit/tasks/test_eol_catalog_refresh.py | 242 ++++++++++++ .../tasks/test_scan_source_cocoapods_merge.py | 3 +- .../unit/tasks/test_scan_source_eol_stamp.py | 4 +- .../tasks/test_scan_source_scope_filter.py | 20 +- .../tests/unit/test_catalog_contracts.py | 6 +- .../features/admin/health/AdminHealthPage.tsx | 2 + .../src/features/admin/health/EolPanel.tsx | 320 +++++++++++++++ .../admin/health/api/adminEolHealthApi.ts | 41 ++ .../admin/health/api/useAdminEolHealth.ts | 27 ++ apps/frontend/src/locales/en/admin.json | 29 ++ apps/frontend/src/locales/ko/admin.json | 29 ++ .../features/admin/health/EolPanel.test.tsx | 190 +++++++++ docs-site/docs/admin-guide/disk-and-health.md | 25 ++ docs-site/docs/reference/env-variables.md | 3 + .../current/admin-guide/disk-and-health.md | 23 ++ .../current/reference/env-variables.md | 3 + 32 files changed, 2271 insertions(+), 18 deletions(-) create mode 100644 apps/backend/alembic/versions/0039_eol_sync_state.py create mode 100644 apps/backend/api/v1/admin/eol.py create mode 100644 apps/backend/integrations/eol_feed.py create mode 100644 apps/backend/models/eol_sync_state.py create mode 100644 apps/backend/services/eol_health_service.py create mode 100644 apps/backend/tasks/eol_catalog_refresh.py create mode 100644 apps/backend/tests/unit/integrations/test_eol_feed.py create mode 100644 apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py create mode 100644 apps/frontend/src/features/admin/health/EolPanel.tsx create mode 100644 apps/frontend/src/features/admin/health/api/adminEolHealthApi.ts create mode 100644 apps/frontend/src/features/admin/health/api/useAdminEolHealth.ts create mode 100644 apps/frontend/tests/unit/features/admin/health/EolPanel.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ff1b2c..919e614a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- **EOL operations: weekly refresh beat + admin health panel.** A weekly + Celery beat re-stamps the component catalog against the newest + endoflife.date snapshot (so release upgrades reach existing rows without + a re-scan, and stamps are cleared when the whitelist shrinks) and — only + when `EOL_REFRESH_ENABLED=true`, off by default — fetches fresh lifecycle + data with a sanity floor that stops a gutted sweep from displacing a good + dataset. The admin/health page gains an endoflife.date snapshot panel + (dataset age with a 180-day stale warning, flagged totals, last tick, + next fire) at `GET /v1/admin/eol/health`. - **End-of-life (EOL) component flagging.** Components matching a curated endoflife.date product whitelist (Spring Boot, Express, Django, Rails, Angular, Vue, Next.js, Symfony, Laravel, Spring Framework) are stamped diff --git a/apps/backend/alembic/versions/0039_eol_sync_state.py b/apps/backend/alembic/versions/0039_eol_sync_state.py new file mode 100644 index 00000000..8b75f365 --- /dev/null +++ b/apps/backend/alembic/versions/0039_eol_sync_state.py @@ -0,0 +1,94 @@ +"""eol_sync_state — single-row endoflife.date refresh status table + +Revision ID: 0039 +Revises: 0038 +Create Date: 2026-07-11 + +Phase: EOL (ops closeout — refresh beat + admin/health EOL panel, Phase M) +PR: feat/eol-ops +Kind: schema (new empty table; no data migration) +Forward-only: yes + +What: + - Create table ``eol_sync_state``:: + id BOOLEAN PK DEFAULT true -- with CHECK (id): single-row + last_synced_at TIMESTAMPTZ -- last SUCCESSFUL feed fetch + last_result VARCHAR(16) -- synced|skipped (fetch half) + skipped_reason VARCHAR(64) -- disabled|refresh_disabled| + -- feed_unavailable| + -- feed_below_sanity_floor| + -- unexpected: + snapshot JSONB -- fetched compact dataset (few KB) + snapshot_date DATE -- snapshot["_snapshot"], denormalised + products_ok INTEGER -- products fetched this sync + products_failed INTEGER -- products that failed this sync + stamped INTEGER -- catalog rows (re)stamped this tick + cleared INTEGER -- stale stamps cleared this tick + duration_ms INTEGER + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- last attempt + - CHECK constraint ``ck_eol_sync_state_singleton`` — same single-row + construction as 0035 (``kev_sync_state``). + +Why: + - Clone of the 0035 pattern with one structural addition: the fetched + dataset itself rides the row (``snapshot`` JSONB). The KEV reconcile + writes straight into ``vulnerabilities`` and needs no dataset store; the + EOL weekly re-stamp pass must prefer the freshest dataset — fetched OR + vendored — without a network call, so the fetched one needs a durable + home. A few KB of JSONB on a single-row table is the cheapest one. + - ``snapshot_date`` is denormalised out of the JSONB so the health panel's + staleness computation (warn tone past 180 days) is a plain DATE read. + - ``stamped`` / ``cleared`` update on EVERY tick — the re-stamp half runs + even when the fetch is disabled (pure-local), unlike the KEV counters. + +Notes: + - Vocabularies stay VARCHAR (task-owned closed sets — 0035 reasoning). + - No extra indexes: single-row table, PK is the only access path. + - No seed row: first tick inserts; "row absent" = "never ran". + - Forward-only per CLAUDE.md §6: ``downgrade()`` raises + ``NotImplementedError``. +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + +from alembic import op + +revision: str = "0039" +down_revision: str | None = "0038" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +NOW = sa.text("now()") + + +def upgrade() -> None: + op.create_table( + "eol_sync_state", + sa.Column("id", sa.Boolean(), primary_key=True, server_default=sa.text("true")), + sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("last_result", sa.String(length=16), nullable=True), + sa.Column("skipped_reason", sa.String(length=64), nullable=True), + sa.Column("snapshot", JSONB(), nullable=True), + sa.Column("snapshot_date", sa.Date(), nullable=True), + sa.Column("products_ok", sa.Integer(), nullable=True), + sa.Column("products_failed", sa.Integer(), nullable=True), + sa.Column("stamped", sa.Integer(), nullable=True), + sa.Column("cleared", sa.Integer(), nullable=True), + sa.Column("duration_ms", sa.Integer(), nullable=True), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=NOW, + ), + sa.CheckConstraint("id", name="ck_eol_sync_state_singleton"), + ) + + +def downgrade() -> None: + raise NotImplementedError("forward-only migration (CLAUDE.md §6)") diff --git a/apps/backend/api/v1/admin/__init__.py b/apps/backend/api/v1/admin/__init__.py index e9d725ba..0ef7a736 100644 --- a/apps/backend/api/v1/admin/__init__.py +++ b/apps/backend/api/v1/admin/__init__.py @@ -15,6 +15,7 @@ - ``backup`` — ``/v1/admin/backup/*`` (chore PR #19: list/trigger/download/restore/delete) - ``trivy`` — ``/v1/admin/trivy/*`` (W6-#43e: vulnerability DB status panel) - ``kev`` — ``/v1/admin/kev/*`` (Phase C: CISA KEV feed sync status panel) + - ``eol`` — ``/v1/admin/eol/*`` (Phase M: endoflife.date snapshot status panel) W6-#43a (ADR-0001): the ``dt`` sub-router was removed when DT was replaced by Trivy (W6-#41); previously-issued DT audit-log rows are preserved as @@ -30,7 +31,7 @@ from core.security import require_super_admin_or_404 -from . import audit, backup, disk, health, kev, scans, teams, trivy, users +from . import audit, backup, disk, eol, health, kev, scans, teams, trivy, users # Apply the super-admin gate at the parent-router level so individual route # signatures stay clean — each route still gets the resolved CurrentUser @@ -51,6 +52,7 @@ router.include_router(backup.router) router.include_router(trivy.router) router.include_router(kev.router) +router.include_router(eol.router) __all__ = ["router"] diff --git a/apps/backend/api/v1/admin/eol.py b/apps/backend/api/v1/admin/eol.py new file mode 100644 index 00000000..e047be98 --- /dev/null +++ b/apps/backend/api/v1/admin/eol.py @@ -0,0 +1,48 @@ +""" +Admin EOL snapshot-health HTTP route — Phase M (PR M-3). + +Endpoint: ``GET /v1/admin/eol/health`` — exposes the endoflife.date dataset +state (effective snapshot date + origin, rule/product counts, live flagged +total, last beat tick outcome, next fire) for the admin/health EOL panel. +Auth gated by the parent admin router (super-admin only; existence-hide for +everyone else — the ``kev`` sub-router convention). + +Pure read: one PK lookup on the single-row ``eol_sync_state`` table, one +partial-index count, plus the in-process vendored-snapshot parse. The writer +is the weekly ``tasks/eol_catalog_refresh`` beat. +""" + +from __future__ import annotations + +import structlog +from fastapi import APIRouter, Depends, Request, Response, status +from sqlalchemy.ext.asyncio import AsyncSession + +from core.db import get_db +from core.security import CurrentUser, require_super_admin_or_404 +from schemas.admin_ops import EolStatusOut +from services.eol_health_service import get_eol_health + +router = APIRouter(prefix="/eol", tags=["admin"]) +log = structlog.get_logger("admin.eol.api") + + +@router.get( + "/health", + response_model=EolStatusOut, + summary="endoflife.date snapshot status (admin) — dataset age / counters / next beat", +) +async def get_eol_health_endpoint( + request: Request, # noqa: ARG001 + session: AsyncSession = Depends(get_db), + actor: CurrentUser = Depends(require_super_admin_or_404()), # noqa: ARG001 +) -> Response: + out = await get_eol_health(session) + return Response( + content=out.model_dump_json(), + status_code=status.HTTP_200_OK, + media_type="application/json", + ) + + +__all__ = ["router"] diff --git a/apps/backend/core/config.py b/apps/backend/core/config.py index f0ffd0e6..965d5129 100644 --- a/apps/backend/core/config.py +++ b/apps/backend/core/config.py @@ -1378,6 +1378,51 @@ def eol_snapshot_path() -> str: return os.getenv("EOL_SNAPSHOT_PATH", "").strip() +def eol_refresh_enabled() -> bool: + """Whether the weekly beat FETCHES fresh data from endoflife.date. + + Default ``false`` — this is NEW egress to a third-party host the product + has never contacted, so it follows the SCANOSS fail-closed posture (only + the exact truthy tokens enable), NOT the KEV default-on one: unlike KEV + — where a stale catalog misses actively-exploited CVEs daily — EOL dates + churn quarterly and the per-release vendored snapshot already bounds + staleness. The beat's RE-STAMP pass runs regardless of this toggle (it + is pure-local). Read at call time (rule #11). + """ + return os.getenv("EOL_REFRESH_ENABLED", "false").strip().lower() in { + "true", + "1", + "yes", + } + + +def eol_feed_url_template() -> str: + """URL template for the endoflife.date per-product API. + + ``{product}`` is substituted with each mapped product slug. Env-only + trust model (same as ``KEV_FEED_URL`` — no user write path, so no + ``core.url_guard``); point it at an internal mirror to keep the egress + inside your network. Read at call time (rule #11). + """ + return os.getenv( + "EOL_FEED_URL_TEMPLATE", "https://endoflife.date/api/{product}.json" + ).strip() + + +def eol_refresh_timeout_seconds() -> int: + """HTTP timeout (seconds) per product request. Default 15s. + + Each product document is a few KB; 15 seconds absorbs a slow corporate + proxy. Bounded ``[1, 120]``. Read at call time (rule #11). + """ + return _int_env( + "EOL_REFRESH_TIMEOUT_SECONDS", + default=15, + minimum=1, + maximum=120, + ) + + # --------------------------------------------------------------------------- # Phase 2 PR #9 — WebSocket gateway configuration accessors. # diff --git a/apps/backend/integrations/eol_feed.py b/apps/backend/integrations/eol_feed.py new file mode 100644 index 00000000..207028f5 --- /dev/null +++ b/apps/backend/integrations/eol_feed.py @@ -0,0 +1,220 @@ +""" +endoflife.date feed client — fresh EOL dataset for the weekly refresh beat. + +Fetches ``https://endoflife.date/api/{product}.json`` for exactly the +products the vendored whitelist (``services/eol/eol_purl_map.json``) +references and assembles the same compact dataset shape the vendored +``eol_snapshot.json`` carries (``{"_snapshot": "", "": +[{cycle, eol, ...}], ...}``) — so the refresh task can treat "fetched" and +"vendored" datasets interchangeably. + +Structural mirror of :mod:`integrations.kev_feed` scaled down to many small +documents instead of one large one: + + * per-product byte ceiling (a product document is a few KB; 2 MiB flags a + misconfigured/compromised mirror without buffering it), + * per-product failures are SKIPPED and counted — one broken product must + not discard the other nine (mirrors the per-entry skip posture), + * a whole-run wall-clock budget bounds the tick even against a slow-drip + server (the BomLens build-eol-index.py 60s budget), + * :class:`EolFeedUnavailable` is raised only when NOTHING was fetched — + the task treats it exactly like a KEV feed outage (skip, retry next + tick, existing verdicts untouched). + +Trust model: the URL template comes exclusively from operator env +(``EOL_FEED_URL_TEMPLATE``, read at call time) — no user write path, so no +``core.url_guard`` (the kev_feed convention). Product slugs come from the +vendored map, never from user input. Logs carry the host only, never the +full URL. +""" + +from __future__ import annotations + +import json +import time +from dataclasses import dataclass +from datetime import UTC, datetime +from typing import Any +from urllib.parse import urlsplit + +import httpx +import structlog + +from core.config import eol_feed_url_template, eol_refresh_timeout_seconds + +log = structlog.get_logger("integrations.eol_feed") + +# Per-product response ceiling. Real documents are a few KB; anything near +# this is a broken or hostile mirror. +_MAX_PRODUCT_BYTES = 2 * 1024 * 1024 + +# Whole-run wall-clock budget (BomLens build-eol-index.py parity). Products +# past the deadline are skipped and counted as failed for this tick. +_RUN_BUDGET_SECONDS = 60 + +# Only the fields the evaluator (and the staleness surface) reads — keeps +# the persisted snapshot the same few-KB shape as the vendored file. +_KEEP_FIELDS = ("cycle", "eol", "releaseDate", "latest", "latestReleaseDate") + +# Ceiling on cycles accepted per product — a real product has dozens. +_MAX_CYCLES_PER_PRODUCT = 500 + + +class EolFeedUnavailable(Exception): + """No product could be fetched at all (network dead, mirror broken). + + Per-product failures below this threshold are NOT this exception — they + are skipped and surfaced in the result's ``failed`` list. + """ + + +@dataclass(frozen=True) +class EolFetchResult: + """Outcome of one feed sweep. + + ``dataset`` is the compact snapshot dict (``_snapshot`` stamped with + today's UTC date); ``fetched`` / ``failed`` list product slugs. + """ + + dataset: dict[str, Any] + fetched: list[str] + failed: list[str] + + +def _safe_host(url: str) -> str: + try: + parsed = urlsplit(url) + except ValueError: + return "" + return parsed.hostname or "" + + +def _compact_cycles(payload: Any) -> list[dict[str, Any]] | None: + """Reduce one product document to the evaluator's field set. + + Returns ``None`` for a document that is not a list of objects — the + product is counted failed rather than persisting garbage. + """ + if not isinstance(payload, list): + return None + compact: list[dict[str, Any]] = [] + for entry in payload[:_MAX_CYCLES_PER_PRODUCT]: + if not isinstance(entry, dict): + continue + compact.append({k: entry[k] for k in _KEEP_FIELDS if k in entry}) + return compact if compact else None + + +def _fetch_product( + client: httpx.Client, url: str, safe_host: str +) -> list[dict[str, Any]] | None: + """One bounded product fetch; ``None`` on any failure (counted, not raised).""" + try: + with client.stream("GET", url) as response: + if response.status_code != 200: + log.warning( + "eol_feed_http_error", + host=safe_host, + status=response.status_code, + ) + return None + buf = bytearray() + for chunk in response.iter_bytes(): + buf.extend(chunk) + if len(buf) > _MAX_PRODUCT_BYTES: + log.warning( + "eol_feed_product_too_large", + host=safe_host, + limit_bytes=_MAX_PRODUCT_BYTES, + ) + return None + except ( + httpx.TimeoutException, + httpx.HTTPError, + # kev_feed precedent: InvalidURL is not an HTTPError subclass, and a + # schemeless URL surfaces as a bare ValueError on httpx 0.28 — both + # would otherwise leak the full URL (possible mirror auth token) in + # a crash. Type name only, host only. + httpx.InvalidURL, + httpx.UnsupportedProtocol, + ValueError, + ) as exc: + log.warning( + "eol_feed_network_failure", + host=safe_host, + error_type=type(exc).__name__, + ) + return None + try: + payload = json.loads(bytes(buf)) + except (ValueError, UnicodeDecodeError): + log.warning("eol_feed_invalid_json", host=safe_host) + return None + return _compact_cycles(payload) + + +def fetch_eol_dataset( + products: list[str], *, http: httpx.Client | None = None +) -> EolFetchResult: + """Fetch the compact dataset for ``products`` (the map's slugs). + + Raises :class:`EolFeedUnavailable` only when EVERY product failed; + partial results return normally with the failures listed (the task's + sanity floor decides whether they are usable). + """ + template = eol_feed_url_template() + safe_host = _safe_host(template.replace("{product}", "probe")) + owned = http is None + client = http or httpx.Client( + timeout=eol_refresh_timeout_seconds(), + follow_redirects=True, + ) + dataset: dict[str, Any] = { + "_snapshot": datetime.now(tz=UTC).date().isoformat() + } + fetched: list[str] = [] + failed: list[str] = [] + deadline = time.monotonic() + _RUN_BUDGET_SECONDS + try: + for product in products: + if time.monotonic() > deadline: + log.warning( + "eol_feed_budget_exhausted", + host=safe_host, + budget_seconds=_RUN_BUDGET_SECONDS, + remaining=len(products) - len(fetched) - len(failed), + ) + failed.extend( + p for p in products if p not in fetched and p not in failed + ) + break + cycles = _fetch_product( + client, template.replace("{product}", product), safe_host + ) + if cycles is None: + failed.append(product) + continue + dataset[product] = cycles + fetched.append(product) + finally: + if owned: + client.close() + + if not fetched: + raise EolFeedUnavailable( + f"no endoflife.date product could be fetched ({len(failed)} failed)" + ) + log.info( + "eol_feed_fetched", + host=safe_host, + fetched=len(fetched), + failed=len(failed), + ) + return EolFetchResult(dataset=dataset, fetched=fetched, failed=failed) + + +__all__ = [ + "EolFeedUnavailable", + "EolFetchResult", + "fetch_eol_dataset", +] diff --git a/apps/backend/models/__init__.py b/apps/backend/models/__init__.py index bb9575b1..6f8c8ea9 100644 --- a/apps/backend/models/__init__.py +++ b/apps/backend/models/__init__.py @@ -37,6 +37,9 @@ class Base(DeclarativeBase): ApprovalStatus, ComponentApproval, ) +from .eol_sync_state import ( # noqa: E402,F401 (imported for metadata side effects) + EolSyncState, +) from .github_app import ( # noqa: E402,F401 (imported for metadata side effects) GitHubAppCredential, GitHubAppInstallation, @@ -94,6 +97,7 @@ class Base(DeclarativeBase): "ComponentApproval", "ComponentDependencyEdge", "ComponentVersion", + "EolSyncState", "GitHubAppCredential", "GitHubAppInstallation", "KevSyncState", diff --git a/apps/backend/models/eol_sync_state.py b/apps/backend/models/eol_sync_state.py new file mode 100644 index 00000000..da2ff604 --- /dev/null +++ b/apps/backend/models/eol_sync_state.py @@ -0,0 +1,80 @@ +""" +EolSyncState — single-row status table for the endoflife.date refresh beat. + +Structural mirror of :mod:`models.kev_sync_state` (the single-row BOOLEAN-PK +``CHECK (id)`` construction, the writer/reader contract, the skip semantics) +with one addition the KEV table does not need: the **fetched snapshot itself** +rides the row (``snapshot`` JSONB, a few KB). The KEV reconcile writes +straight into ``vulnerabilities`` and needs no dataset store; the EOL refresh +must keep the freshest dataset somewhere durable so the weekly re-stamp pass +can prefer it over the (potentially older) snapshot vendored with the release +— without any network call on air-gapped installs. + +Writer / reader contract: + - **Writer**: ``tasks/eol_catalog_refresh`` UPSERTs the one row at the end + of every tick. The fetch half updates ``last_synced_at`` / ``snapshot`` / + ``snapshot_date`` / ``products_ok`` / ``products_failed`` only on a + SUCCESSFUL fetch; the re-stamp half's counters (``stamped`` / + ``cleared``) update on every tick — the re-stamp runs even when the + fetch is disabled or fails (pure-local pass). + - **Reader**: ``services.eol_health_service`` (admin/health EOL panel) and + the refresh task itself (effective-dataset resolution). + +Semantics: + - ``last_result`` is the FETCH outcome: ``"synced"`` | ``"skipped"`` + (closed task-owned vocabulary, VARCHAR not native ENUM — the + kev_sync_state reasoning). + - ``skipped_reason``: ``disabled`` / ``refresh_disabled`` / + ``feed_unavailable`` / ``feed_below_sanity_floor`` / + ``unexpected:``. + - ``snapshot_date`` mirrors the stored ``snapshot["_snapshot"]`` as a DATE + so the health panel's staleness maths never parses JSONB. + +No PII, no tenant scoping: system-global operational metadata, admin-only +read surface. +""" + +from __future__ import annotations + +from datetime import date, datetime +from typing import Any + +from sqlalchemy import Boolean, CheckConstraint, Date, DateTime, Integer, String, text +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column + +from . import Base +from .scan import NOW + + +class EolSyncState(Base): + """The one status row for the endoflife.date refresh beat (0039).""" + + __tablename__ = "eol_sync_state" + + id: Mapped[bool] = mapped_column(Boolean, primary_key=True, server_default=text("true")) + last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + last_result: Mapped[str | None] = mapped_column(String(16), nullable=True) + skipped_reason: Mapped[str | None] = mapped_column(String(64), nullable=True) + # The fetched compact dataset (same shape as the vendored + # eol_snapshot.json, a few KB). NULL until the first successful fetch. + snapshot: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True) + snapshot_date: Mapped[date | None] = mapped_column(Date, nullable=True) + products_ok: Mapped[int | None] = mapped_column(Integer, nullable=True) + products_failed: Mapped[int | None] = mapped_column(Integer, nullable=True) + stamped: Mapped[int | None] = mapped_column(Integer, nullable=True) + cleared: Mapped[int | None] = mapped_column(Integer, nullable=True) + duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=NOW, + onupdate=NOW, + ) + + __table_args__ = ( + CheckConstraint("id", name="ck_eol_sync_state_singleton"), + ) + + +__all__ = ["EolSyncState"] diff --git a/apps/backend/schemas/admin_ops.py b/apps/backend/schemas/admin_ops.py index e6191f5e..51e1ac1b 100644 --- a/apps/backend/schemas/admin_ops.py +++ b/apps/backend/schemas/admin_ops.py @@ -34,7 +34,7 @@ from __future__ import annotations import uuid -from datetime import datetime +from datetime import date, datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -513,6 +513,118 @@ class KevFeedStatusOut(BaseModel): ) +# Closed fetch-outcome vocabulary owned by tasks/eol_catalog_refresh (same +# posture as KevSyncResult above). +EolSyncResult = Literal["synced", "skipped"] + + +class EolStatusOut(BaseModel): + """ + Response of ``GET /v1/admin/eol/health`` — Phase M EOL snapshot panel. + + Structural mirror of :class:`KevFeedStatusOut` with the dataset-specific + additions: the EFFECTIVE snapshot date (max of vendored and fetched — the + dataset the weekly re-stamp actually used), its origin, and the re-stamp + counters that update on every tick (the re-stamp half runs even when the + fetch is disabled). + """ + + enabled: bool = Field( + description="Mirrors ``EOL_ENABLED`` — the flagging feature itself.", + ) + refresh_enabled: bool = Field( + description=( + "Mirrors ``EOL_REFRESH_ENABLED`` — the optional live fetch. " + "``false`` (the default) means verdicts come from the vendored " + "snapshot only; the weekly re-stamp still runs." + ), + ) + snapshot_date: date | None = Field( + default=None, + description=( + "Date of the EFFECTIVE dataset — the newer of the vendored " + "snapshot and the last fetched one. The panel's staleness " + "signal (warn past 180 days). ``null`` when no dataset loads." + ), + ) + snapshot_origin: Literal["vendored", "feed"] | None = Field( + default=None, + description="Where the effective dataset came from.", + ) + rule_count: int = Field( + default=0, + ge=0, + description="Rules in the vendored purl→product whitelist.", + ) + product_count: int = Field( + default=0, + ge=0, + description="Products carried by the effective dataset.", + ) + eol_flagged_total: int | None = Field( + default=None, + ge=0, + description=( + "Live count of catalog component versions currently " + "``eol_state='eol'`` (rides the partial index " + "``ix_component_versions_eol``). ``null`` only on DB-read " + "degrade." + ), + ) + last_synced_at: datetime | None = Field( + default=None, + description="Timestamp of the last SUCCESSFUL feed fetch.", + ) + last_attempt_at: datetime | None = Field( + default=None, + description=( + "Timestamp of the most recent beat tick of any outcome " + "(``eol_sync_state.updated_at``). ``null`` = never ran." + ), + ) + last_result: EolSyncResult | None = Field( + default=None, + description="Fetch outcome of the most recent tick.", + ) + skipped_reason: str | None = Field( + default=None, + description=( + "Why the most recent fetch skipped: ``disabled`` / " + "``refresh_disabled`` / ``feed_unavailable`` / " + "``feed_below_sanity_floor`` / ``unexpected:``." + ), + ) + stamped: int | None = Field( + default=None, + ge=0, + description="Catalog rows (re)stamped on the most recent tick.", + ) + cleared: int | None = Field( + default=None, + ge=0, + description="Stale stamps cleared on the most recent tick.", + ) + duration_ms: int | None = Field( + default=None, + ge=0, + description="Wall-clock duration of the most recent tick, in milliseconds.", + ) + next_refresh_at: datetime | None = Field( + default=None, + description=( + "Next fire time of the weekly EOL beat, derived from the live " + "Celery beat schedule. ``null`` if the entry cannot be resolved." + ), + ) + feed_host: str | None = Field( + default=None, + description=( + "Host of ``EOL_FEED_URL_TEMPLATE`` — host only, never the full " + "URL." + ), + ) + + __all__ = [ "AdminDiskItem", "AdminDiskOut", @@ -524,6 +636,8 @@ class KevFeedStatusOut(BaseModel): "AuditTargetTable", "HealthComponent", "HealthStatus", + "EolStatusOut", + "EolSyncResult", "KevFeedStatusOut", "KevSyncResult", "ScanStatus", diff --git a/apps/backend/services/eol_health_service.py b/apps/backend/services/eol_health_service.py new file mode 100644 index 00000000..3b55eb6d --- /dev/null +++ b/apps/backend/services/eol_health_service.py @@ -0,0 +1,169 @@ +""" +EOL snapshot health service — Phase M (admin/health EOL panel). + +Assembles the ``GET /v1/admin/eol/health`` payload from four sources +(structural mirror of :mod:`services.kev_health_service`, including the +graceful degrade to a config-only payload and the no-cache reasoning): + + * the single ``eol_sync_state`` row (written by + ``tasks/eol_catalog_refresh`` at the end of every weekly tick), + * the EFFECTIVE dataset — the newer of the vendored/override snapshot and + the last fetched one — whose date is the panel's staleness signal, + * a live count of ``component_versions.eol_state = 'eol'`` rows (rides + the partial index ``ix_component_versions_eol``), + * runtime config + the LIVE Celery beat schedule (next fire derived from + the crontab under the shared ``EOL_BEAT_ENTRY_NAME``). +""" + +from __future__ import annotations + +from datetime import UTC, date, datetime +from typing import Literal, cast +from urllib.parse import urlsplit + +import structlog +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from core.config import eol_enabled, eol_feed_url_template, eol_refresh_enabled +from models import ComponentVersion, EolSyncState +from schemas.admin_ops import EolStatusOut, EolSyncResult +from services.eol.eol_catalog import load_dataset, load_rules + +log = structlog.get_logger("admin.eol_health.service") + + +def _now() -> datetime: + return datetime.now(tz=UTC) + + +def _feed_host() -> str | None: + """Host of the feed URL template — host only, never the full URL.""" + try: + return urlsplit( + eol_feed_url_template().replace("{product}", "probe") + ).hostname + except ValueError: # pragma: no cover — urlsplit rejects almost nothing + return None + + +def compute_next_refresh_at(now: datetime | None = None) -> datetime | None: + """Next fire of the EOL beat, from the live schedule (KEV idiom).""" + try: + from tasks.celery_app import EOL_BEAT_ENTRY_NAME, celery_app + + entry = celery_app.conf.beat_schedule.get(EOL_BEAT_ENTRY_NAME) + if not entry: + return None + schedule = entry["schedule"] + reference = now if now is not None else _now() + start, delta, _ = schedule.remaining_delta(reference) + return cast(datetime, start + delta) + except Exception as exc: # noqa: BLE001 — panel field, never worth a 500 + log.warning("eol_next_refresh_derivation_failed", error=str(exc)[:300]) + return None + + +def _parse_snapshot_date(raw: str | None) -> date | None: + if not raw: + return None + try: + return date.fromisoformat(raw) + except ValueError: + return None + + +def _effective_snapshot( + row: EolSyncState | None, +) -> tuple[date | None, Literal["vendored", "feed"] | None, int]: + """(date, origin, product_count) of the newer dataset — vendored vs fetched.""" + vendored = load_dataset() + vendored_date = _parse_snapshot_date(vendored.snapshot) if vendored else None + fetched_date = row.snapshot_date if row is not None else None + + if vendored_date is None and fetched_date is None: + return None, None, 0 + if fetched_date is not None and ( + vendored_date is None or fetched_date >= vendored_date + ): + products = 0 + if row is not None and isinstance(row.snapshot, dict): + products = sum(1 for k in row.snapshot if not k.startswith("_")) + return fetched_date, "feed", products + assert vendored is not None # implied by vendored_date is not None + return vendored_date, "vendored", len(vendored.products) + + +async def get_eol_health(session: AsyncSession) -> EolStatusOut: + """Public entry point — the admin endpoint calls this. + + Row absent (beat never ran) is a legitimate state; DB failures degrade + to a config-plus-vendored payload with a WARNING. + """ + enabled = eol_enabled() + refresh_enabled = eol_refresh_enabled() + host = _feed_host() + next_refresh = compute_next_refresh_at() + rule_count = len(load_rules()) + + try: + row = await session.get(EolSyncState, True) + flagged = ( + await session.execute( + select(func.count()) + .select_from(ComponentVersion) + .where(ComponentVersion.eol_state == "eol") + ) + ).scalar_one() + snapshot_date, origin, product_count = _effective_snapshot(row) + + if row is None: + return EolStatusOut( + enabled=enabled, + refresh_enabled=refresh_enabled, + snapshot_date=snapshot_date, + snapshot_origin=origin, + rule_count=rule_count, + product_count=product_count, + eol_flagged_total=int(flagged), + next_refresh_at=next_refresh, + feed_host=host, + ) + + return EolStatusOut( + enabled=enabled, + refresh_enabled=refresh_enabled, + snapshot_date=snapshot_date, + snapshot_origin=origin, + rule_count=rule_count, + product_count=product_count, + eol_flagged_total=int(flagged), + last_synced_at=row.last_synced_at, + last_attempt_at=row.updated_at, + last_result=cast(EolSyncResult | None, row.last_result), + skipped_reason=row.skipped_reason, + stamped=row.stamped, + cleared=row.cleared, + duration_ms=row.duration_ms, + next_refresh_at=next_refresh, + feed_host=host, + ) + except Exception as exc: # noqa: BLE001 — last-resort graceful degrade + log.warning("eol_health_read_failed", error=str(exc)[:300]) + snapshot_date, origin, product_count = _effective_snapshot(None) + return EolStatusOut( + enabled=enabled, + refresh_enabled=refresh_enabled, + snapshot_date=snapshot_date, + snapshot_origin=origin, + rule_count=rule_count, + product_count=product_count, + next_refresh_at=next_refresh, + feed_host=host, + ) + + +__all__ = [ + "compute_next_refresh_at", + "get_eol_health", +] diff --git a/apps/backend/tasks/celery_app.py b/apps/backend/tasks/celery_app.py index f0fd4308..86e8c7f5 100644 --- a/apps/backend/tasks/celery_app.py +++ b/apps/backend/tasks/celery_app.py @@ -74,6 +74,11 @@ # Known Exploited Vulnerabilities feed. Backs the Vulnerabilities tab's # ``sort=priority`` ranking (KEV → severity → EPSS). "tasks.kev_catalog_refresh", + # Phase M — endoflife.date refresh beat: weekly re-stamp of the + # ``component_versions.eol_*`` columns (migration 0038) against the + # newest snapshot (vendored or, when EOL_REFRESH_ENABLED, freshly + # fetched). Backs the Components tab's EOL badge/filter. + "tasks.eol_catalog_refresh", # W6-#44 — Trivy DB weekly refresh beat. Pairs with the worker-boot # bootstrap hook (tasks.trivy_db_bootstrap) so a fresh worker picks up # the DB once at start, and a running deployment refreshes weekly to @@ -95,6 +100,11 @@ # one vocabulary, one owner). KEV_BEAT_ENTRY_NAME = "kev-catalog-refresh-daily" +# Beat-schedule key of the EOL catalog refresh entry — shared with +# ``services.eol_health_service`` for the same live-crontab derivation +# (see KEV_BEAT_ENTRY_NAME's rationale above). +EOL_BEAT_ENTRY_NAME = "eol-catalog-refresh-weekly" + def _build_beat_schedule() -> dict[str, dict[str, object]]: """ @@ -180,6 +190,16 @@ def _build_beat_schedule() -> dict[str, dict[str, object]]: "task": "trustedoss.kev_catalog_refresh", "schedule": crontab(minute=45, hour=1), }, + # Phase M — endoflife.date refresh + catalog re-stamp. Weekly is + # proportionate to EOL churn (lifecycle dates move quarterly, not + # daily). Sunday 02:15 UTC sits between the daily KEV tick (01:45) + # and the weekly Trivy refresh (Sun 03:00) so no two beats share a + # tick; the work is bounded (≤10 tiny fetches when enabled + one + # whitelist-bounded UPDATE pass). + EOL_BEAT_ENTRY_NAME: { + "task": "trustedoss.eol_catalog_refresh", + "schedule": crontab(minute=15, hour=2, day_of_week="sun"), + }, # W6-#44 — weekly Trivy DB refresh. Sunday 03:00 UTC was chosen as # the lowest-traffic window on the typical enterprise cluster: 03:00 # UTC is overnight in the Americas, early morning in EMEA, and diff --git a/apps/backend/tasks/eol_catalog_refresh.py b/apps/backend/tasks/eol_catalog_refresh.py new file mode 100644 index 00000000..2cf2d41b --- /dev/null +++ b/apps/backend/tasks/eol_catalog_refresh.py @@ -0,0 +1,367 @@ +""" +endoflife.date catalog refresh — weekly Celery beat (Phase M, PR M-3). + +Structural mirror of :mod:`tasks.kev_catalog_refresh` with one architectural +difference: the tick has TWO halves with different network postures. + + 1. **Fetch half** (env-gated, default OFF — ``EOL_REFRESH_ENABLED``): + pulls fresh per-product data from endoflife.date via + :func:`integrations.eol_feed.fetch_eol_dataset`, applies a sanity floor + (at least half the map's products must have fetched — a gutted sweep + must never displace a good dataset), and persists the accepted snapshot + into the ``eol_sync_state`` row (JSONB, a few KB). + 2. **Re-stamp half** (ALWAYS runs — pure local, no egress): resolves the + effective dataset as the NEWER of (vendored/override snapshot, the + sync-state row's fetched snapshot) and re-evaluates every catalog row + that either matches a whitelist prefix or already carries a stamp. + This is what makes an image upgrade (newer vendored snapshot) reach + existing rows on air-gapped installs, and what clears stale stamps + when the whitelist shrinks — the per-scan persist hook deliberately + never clears (its job is enrichment, not reconciliation). + +Staleness contract: the per-scan hook stamps from the vendored/override +snapshot only (no DB read on the persist path); a fetched-but-newer dataset +reaches rows on the next weekly tick, so persist-time staleness is bounded +by the beat cadence. + +Failure isolation, idempotency, air-gap posture and the status-row write all +follow the KEV task line by line (never raises into the beat; changed-value +guards make re-runs no-ops; the ``finally`` UPSERT covers every exit path). +""" + +from __future__ import annotations + +import time +from datetime import UTC, date, datetime +from typing import Any + +import structlog +from sqlalchemy import or_, select +from sqlalchemy.dialects.postgresql import insert as pg_insert + +from core.config import eol_enabled, eol_refresh_enabled +from integrations.eol_feed import EolFeedUnavailable, fetch_eol_dataset +from models import ComponentVersion, EolSyncState +from services.eol.eol_catalog import ( + EolDataset, + load_dataset, + load_rules, + stamp_component_version, +) +from tasks.celery_app import celery_app + +log = structlog.get_logger("tasks.eol_catalog_refresh") + + +def _snapshot_to_dataset(raw: Any) -> EolDataset | None: + """Parse a stored sync-state snapshot dict into an EolDataset (or None).""" + if not isinstance(raw, dict): + return None + snapshot = raw.get("_snapshot") + if not isinstance(snapshot, str) or not snapshot: + return None + products = { + key: value + for key, value in raw.items() + if not key.startswith("_") and isinstance(value, list) + } + if not products: + return None + return EolDataset(snapshot=snapshot, products=products) + + +def _newer_dataset( + vendored: EolDataset | None, fetched: EolDataset | None +) -> EolDataset | None: + """Prefer the dataset with the more recent ``_snapshot`` date. + + Tie goes to the FETCHED one (it reflects a live confirmation of the same + day's data). Unparseable snapshot dates lose to parseable ones. + """ + if vendored is None: + return fetched + if fetched is None: + return vendored + + def _parse(dataset: EolDataset) -> date | None: + try: + return date.fromisoformat(dataset.snapshot) + except ValueError: + return None + + vendored_date, fetched_date = _parse(vendored), _parse(fetched) + if fetched_date is None: + return vendored + if vendored_date is None: + return fetched + return vendored if vendored_date > fetched_date else fetched + + +def _clear_stamp(row: ComponentVersion) -> bool: + """NULL every eol_* column for a row no rule covers any more.""" + changed = False + for attr in ("eol_state", "eol_product", "eol_cycle", "eol_date", "eol_source"): + if getattr(row, attr) is not None: + setattr(row, attr, None) + changed = True + if changed: + row.eol_evaluated_at = datetime.now(tz=UTC) + return changed + + +def _sync_state_values(summary: dict[str, Any], now: datetime) -> dict[str, Any]: + """Map a tick summary onto the ``eol_sync_state`` UPSERT column set. + + Unlike the KEV mapping, the re-stamp counters (``stamped`` / ``cleared``) + and ``duration_ms`` ride EVERY tick — the re-stamp half runs regardless + of the fetch outcome. Only the fetch-derived fields (``last_synced_at``, + ``snapshot``, ``snapshot_date``, ``products_*``) are withheld on a + skipped fetch so the last-good snapshot survives. + """ + values: dict[str, Any] = { + "id": True, + "stamped": summary["stamped"], + "cleared": summary["cleared"], + "duration_ms": int(round(summary["duration_seconds"] * 1000)), + "updated_at": now, + } + if summary["skipped"]: + values["last_result"] = "skipped" + values["skipped_reason"] = summary["skipped_reason"] + return values + values.update( + { + "last_result": "synced", + "skipped_reason": None, + "last_synced_at": now, + "snapshot": summary["snapshot"], + "snapshot_date": summary["snapshot_date"], + "products_ok": summary["products_ok"], + "products_failed": summary["products_failed"], + } + ) + return values + + +def _persist_sync_state(summary: dict[str, Any]) -> None: + """UPSERT the single ``eol_sync_state`` row (kev_catalog_refresh idiom).""" + from core.db import sync_session_scope + + values = _sync_state_values(summary, datetime.now(tz=UTC)) + stmt = ( + pg_insert(EolSyncState) + .values(values) + .on_conflict_do_update( + index_elements=[EolSyncState.id], + set_={k: v for k, v in values.items() if k != "id"}, + ) + ) + with sync_session_scope() as session: + session.execute(stmt) + session.commit() + + +def _fetch_half(summary: dict[str, Any]) -> EolDataset | None: + """Run the env-gated fetch; returns the accepted dataset or ``None``. + + Every skip flavour mirrors the KEV task's vocabulary; the sanity floor + is proportional (≥ half the map's products) because the population is a + 10-product whitelist, not a 1,600-entry monotonic catalog. + """ + if not eol_refresh_enabled(): + summary["skipped"] = True + summary["skipped_reason"] = "refresh_disabled" + return None + + products = [rule.product for rule in load_rules()] + # Order-preserving dedup — several rules share a product (spring-boot). + products = list(dict.fromkeys(products)) + if not products: + summary["skipped"] = True + summary["skipped_reason"] = "no_products_mapped" + return None + + try: + result = fetch_eol_dataset(products) + except EolFeedUnavailable as exc: + summary["skipped"] = True + summary["skipped_reason"] = "feed_unavailable" + log.warning("eol_catalog_refresh_feed_unavailable", error=str(exc)[:300]) + return None + + floor = (len(products) + 1) // 2 + if len(result.fetched) < floor: + # A mostly-failed sweep must never displace the last good snapshot — + # the missing products would all evaluate to "unknown" and a later + # good sweep would churn them back. Same posture as an outage. + summary["skipped"] = True + summary["skipped_reason"] = "feed_below_sanity_floor" + log.warning( + "eol_catalog_refresh_feed_below_sanity_floor", + fetched=len(result.fetched), + floor=floor, + ) + return None + + summary["products_ok"] = len(result.fetched) + summary["products_failed"] = len(result.failed) + summary["snapshot"] = result.dataset + summary["snapshot_date"] = result.dataset.get("_snapshot") + return _snapshot_to_dataset(result.dataset) + + +@celery_app.task( # type: ignore[misc] + name="trustedoss.eol_catalog_refresh", + bind=True, + # No autoretry — weekly cadence absorbs transient failures (KEV idiom). + max_retries=0, +) +def refresh_eol_catalog(self: Any) -> dict[str, Any]: + """Fetch (optional) + re-stamp the component catalog's EOL columns. + + Returns a summary dict; never raises into the beat:: + + { + "skipped": bool, # the FETCH half's outcome + "skipped_reason": str|None, + "products_ok": int, "products_failed": int, + "snapshot": dict|None, # accepted dataset (persisted) + "snapshot_date": str|None, + "stamped": int, # rows (re)stamped this tick + "cleared": int, # stale stamps cleared this tick + "duration_seconds": float, + } + """ + structlog.contextvars.bind_contextvars( + task_name="eol_catalog_refresh", + task_id=str(self.request.id) if self and self.request else None, + ) + summary: dict[str, Any] = { + "skipped": False, + "skipped_reason": None, + "products_ok": 0, + "products_failed": 0, + "snapshot": None, + "snapshot_date": None, + "stamped": 0, + "cleared": 0, + "duration_seconds": 0.0, + } + started = time.monotonic() + try: + if not eol_enabled(): + # Feature fully off — no fetch, no re-stamp, and the status row + # records the disabled tick. + summary["skipped"] = True + summary["skipped_reason"] = "disabled" + log.info("eol_catalog_refresh_disabled") + return summary + + fetched_dataset = _fetch_half(summary) + + from core.db import sync_session_scope + + with sync_session_scope() as session: + # Effective dataset: newest of (vendored/override, fetched-now, + # fetched-earlier from the status row). A fetch this tick already + # IS the newest network state; otherwise fall back to whatever + # the row stored on a previous tick. + stored_dataset: EolDataset | None = None + if fetched_dataset is None: + state_row = session.get(EolSyncState, True) + if state_row is not None: + stored_dataset = _snapshot_to_dataset(state_row.snapshot) + effective = _newer_dataset( + load_dataset(), fetched_dataset or stored_dataset + ) + if effective is None: + # No usable dataset anywhere — nothing to re-stamp against. + if not summary["skipped"]: + summary["skipped"] = True + summary["skipped_reason"] = "no_dataset" + log.warning("eol_catalog_refresh_no_dataset") + return summary + + rules = load_rules() + now = datetime.now(tz=UTC) + today = now.date() + # Bounded SELECT: rows matching any whitelist prefix (the stamp + # set) OR already stamped (the clear set — covers a shrunk map). + # %40-encoded npm scopes are matched by a second LIKE per rule + # whose prefix contains '@' (cdxgen emits both spellings). + like_clauses = [] + for rule in rules: + like_clauses.append( + ComponentVersion.purl_with_version.like(rule.purl_prefix + "%") + ) + if "@" in rule.purl_prefix: + like_clauses.append( + ComponentVersion.purl_with_version.like( + rule.purl_prefix.replace("@", "%40") + "%" + ) + ) + like_clauses.append(ComponentVersion.eol_state.is_not(None)) + rows = ( + session.execute( + select(ComponentVersion).where(or_(*like_clauses)) + ) + .scalars() + .all() + ) + + from services.eol.eol_catalog import evaluate + + for row in rows: + verdict = evaluate( + row.purl_with_version, + row.version, + rules=rules, + dataset=effective, + today=today, + ) + if verdict is None: + # No rule covers this purl (map shrank) — clear any + # stale stamp; an unstamped row is untouched. + if _clear_stamp(row): + summary["cleared"] += 1 + continue + if stamp_component_version(row, verdict, now): + summary["stamped"] += 1 + + session.commit() + + summary["duration_seconds"] = time.monotonic() - started + log.info( + "eol_catalog_refresh_complete", + **{k: v for k, v in summary.items() if k != "snapshot"}, + ) + return summary + except Exception as exc: # noqa: BLE001 — task must not raise into the beat + summary["skipped"] = True + summary["skipped_reason"] = f"unexpected:{type(exc).__name__}" + summary["duration_seconds"] = time.monotonic() - started + log.warning("eol_catalog_refresh_unexpected_error", error=str(exc)[:300]) + return summary + finally: + # Status-row write covers EVERY exit path from one call site + # (kev_catalog_refresh idiom). Best-effort by contract. + try: + _persist_sync_state(summary) + except Exception as exc: # noqa: BLE001 — best-effort status write + log.warning( + "eol_sync_state_persist_failed", + error=str(exc)[:300], + ) + structlog.contextvars.unbind_contextvars("task_name", "task_id") + + +__all__ = [ + "refresh_eol_catalog", + # Exposed for unit tests. + "_clear_stamp", + "_fetch_half", + "_newer_dataset", + "_persist_sync_state", + "_snapshot_to_dataset", + "_sync_state_values", +] diff --git a/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py b/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py index 0592d4f8..3ae1f948 100644 --- a/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py +++ b/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py @@ -82,7 +82,9 @@ def _fake_run(cmd: list[str], **kwargs: Any) -> SimpleNamespace: return SimpleNamespace(returncode=0, stdout=b"", stderr=b"") monkeypatch.setattr(cdxgen, "run_with_line_streaming", _fake_run) - monkeypatch.setattr(cdxgen.shutil, "which", lambda name: f"/usr/bin/{name}") + monkeypatch.setattr( + "integrations.cdxgen.shutil.which", lambda name: f"/usr/bin/{name}" + ) return calls diff --git a/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py b/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py index 014d7531..76ed5c68 100644 --- a/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py +++ b/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py @@ -20,6 +20,7 @@ from __future__ import annotations from pathlib import Path +from typing import Any from integrations.cocoapods_lockfile import ( MAX_PODS, @@ -194,7 +195,7 @@ def _data() -> CocoapodsLockfileData: def test_merge_appends_components_edges_and_stamp() -> None: - sbom = { + sbom: dict[str, Any] = { "components": [{"purl": "pkg:swift/github.com/apple/swift-log@1.5.3"}], "dependencies": [], "metadata": {}, @@ -214,7 +215,7 @@ def test_merge_appends_components_edges_and_stamp() -> None: def test_merge_noop_when_pods_already_present() -> None: - sbom = { + sbom: dict[str, Any] = { "components": [{"purl": "pkg:cocoapods/Existing@1.0.0"}], "dependencies": [], } diff --git a/apps/backend/tests/unit/integrations/test_eol_feed.py b/apps/backend/tests/unit/integrations/test_eol_feed.py new file mode 100644 index 00000000..eb97c6e6 --- /dev/null +++ b/apps/backend/tests/unit/integrations/test_eol_feed.py @@ -0,0 +1,136 @@ +"""Unit tests — endoflife.date feed client (Phase M, PR M-3). + +Same harness as ``test_kev_feed.py``: an ``httpx.MockTransport``-backed +client injected through the ``http=`` parameter, so no network ever runs. +Pinned contracts: + + * per-product failures are skipped and counted — one broken product never + discards the others (the KEV per-entry posture at product granularity); + * :class:`EolFeedUnavailable` fires only when NOTHING fetched; + * the per-product byte ceiling and the non-list document guard; + * the assembled dataset carries ``_snapshot`` and only the evaluator's + field set (compact — the persisted JSONB stays a few KB). +""" + +from __future__ import annotations + +import json + +import httpx +import pytest + +from integrations.eol_feed import ( + EolFeedUnavailable, + fetch_eol_dataset, +) + +_GOOD_CYCLES = [ + { + "cycle": "4", + "eol": False, + "latest": "4.21.0", + "releaseDate": "2014-04-09", + "link": "https://example.com/ignored", # NOT in _KEEP_FIELDS + "lts": True, # NOT in _KEEP_FIELDS + }, + {"cycle": "3", "eol": True}, +] + + +def _client(handler: object) -> httpx.Client: + return httpx.Client(transport=httpx.MockTransport(handler)) # type: ignore[arg-type] + + +def test_happy_path_compacts_fields_and_stamps_snapshot() -> None: + def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, json=_GOOD_CYCLES) + + result = fetch_eol_dataset(["express", "django"], http=_client(handler)) + assert result.fetched == ["express", "django"] + assert result.failed == [] + assert isinstance(result.dataset["_snapshot"], str) + express = result.dataset["express"] + assert express[0] == { + "cycle": "4", + "eol": False, + "latest": "4.21.0", + "releaseDate": "2014-04-09", + } # link/lts stripped — compact persisted shape + + +def test_partial_failure_skips_and_counts() -> None: + def handler(request: httpx.Request) -> httpx.Response: + if "django" in str(request.url): + return httpx.Response(500) + return httpx.Response(200, json=_GOOD_CYCLES) + + result = fetch_eol_dataset(["express", "django"], http=_client(handler)) + assert result.fetched == ["express"] + assert result.failed == ["django"] + assert "django" not in result.dataset + + +def test_all_failed_raises_unavailable() -> None: + def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(503) + + with pytest.raises(EolFeedUnavailable): + fetch_eol_dataset(["express", "django"], http=_client(handler)) + + +def test_non_list_document_counts_as_failed() -> None: + def handler(request: httpx.Request) -> httpx.Response: + if "django" in str(request.url): + return httpx.Response(200, json={"not": "a list"}) + return httpx.Response(200, json=_GOOD_CYCLES) + + result = fetch_eol_dataset(["express", "django"], http=_client(handler)) + assert result.failed == ["django"] + + +def test_invalid_json_counts_as_failed() -> None: + def handler(request: httpx.Request) -> httpx.Response: + if "django" in str(request.url): + return httpx.Response(200, content=b"{ not json") + return httpx.Response(200, json=_GOOD_CYCLES) + + result = fetch_eol_dataset(["express", "django"], http=_client(handler)) + assert result.failed == ["django"] + + +def test_oversized_product_document_counts_as_failed() -> None: + huge = json.dumps([{"cycle": str(i), "eol": False} for i in range(200_000)]) + assert len(huge) > 2 * 1024 * 1024 # over the per-product ceiling + + def handler(request: httpx.Request) -> httpx.Response: + if "django" in str(request.url): + return httpx.Response(200, content=huge.encode()) + return httpx.Response(200, json=_GOOD_CYCLES) + + result = fetch_eol_dataset(["express", "django"], http=_client(handler)) + assert result.failed == ["django"] + assert result.fetched == ["express"] + + +def test_network_error_counts_as_failed_without_url_leak() -> None: + def handler(request: httpx.Request) -> httpx.Response: + if "django" in str(request.url): + raise httpx.ConnectError("boom", request=request) + return httpx.Response(200, json=_GOOD_CYCLES) + + result = fetch_eol_dataset(["express", "django"], http=_client(handler)) + assert result.failed == ["django"] + + +def test_template_env_override(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv( + "EOL_FEED_URL_TEMPLATE", "https://mirror.internal/eol/{product}.json" + ) + seen: list[str] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(str(request.url)) + return httpx.Response(200, json=_GOOD_CYCLES) + + fetch_eol_dataset(["express"], http=_client(handler)) + assert seen == ["https://mirror.internal/eol/express.json"] diff --git a/apps/backend/tests/unit/openapi_endpoints.json b/apps/backend/tests/unit/openapi_endpoints.json index a069c3f8..b56dd473 100644 --- a/apps/backend/tests/unit/openapi_endpoints.json +++ b/apps/backend/tests/unit/openapi_endpoints.json @@ -77,6 +77,7 @@ "name" ], "GET /v1/admin/disk": [], + "GET /v1/admin/eol/health": [], "GET /v1/admin/health": [], "GET /v1/admin/kev/health": [], "GET /v1/admin/scans": [ diff --git a/apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py b/apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py new file mode 100644 index 00000000..e7d31a2d --- /dev/null +++ b/apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py @@ -0,0 +1,242 @@ +"""Unit tests — endoflife.date refresh task internals (Phase M, PR M-3). + +DB-free units over the task's pure helpers plus the fetch-half gating, +mirroring ``test_kev_sync_state_persist.py``'s scope split: the real +UPSERT-against-Postgres behaviour belongs to an integration test; here we +pin the contracts that make the tick safe — + + * ``_sync_state_values``: the re-stamp counters ride EVERY tick (the + re-stamp half runs even on a skipped fetch — unlike KEV), while the + fetch-derived fields are withheld on a skip so the last-good snapshot + survives the UPSERT; + * ``_newer_dataset`` / ``_snapshot_to_dataset``: effective-dataset + resolution (vendored vs fetched, tie → fetched, garbage loses); + * ``_fetch_half``: default-OFF gate, feed-outage skip, and the + proportional sanity floor (≥ half the mapped products) that stops a + gutted sweep from displacing a good snapshot; + * ``_clear_stamp``: NULLs every eol_* column exactly once (idempotent). +""" + +from __future__ import annotations + +from datetime import UTC, date, datetime +from typing import Any + +import pytest + +from services.eol.eol_catalog import EolDataset +from tasks.eol_catalog_refresh import ( + _clear_stamp, + _fetch_half, + _newer_dataset, + _snapshot_to_dataset, + _sync_state_values, +) + +NOW = datetime(2026, 7, 12, 2, 15, 3, tzinfo=UTC) + + +def _base_summary(**overrides: Any) -> dict[str, Any]: + summary: dict[str, Any] = { + "skipped": False, + "skipped_reason": None, + "products_ok": 10, + "products_failed": 0, + "snapshot": {"_snapshot": "2026-07-12", "express": []}, + "snapshot_date": "2026-07-12", + "stamped": 7, + "cleared": 2, + "duration_seconds": 1.2345, + } + summary.update(overrides) + return summary + + +# --------------------------------------------------------------------------- +# _sync_state_values +# --------------------------------------------------------------------------- + + +def test_synced_values_carry_everything() -> None: + values = _sync_state_values(_base_summary(), NOW) + assert values == { + "id": True, + "last_result": "synced", + "skipped_reason": None, + "last_synced_at": NOW, + "snapshot": {"_snapshot": "2026-07-12", "express": []}, + "snapshot_date": "2026-07-12", + "products_ok": 10, + "products_failed": 0, + "stamped": 7, + "cleared": 2, + "duration_ms": 1234, # 1.2345s → rounded integer ms + "updated_at": NOW, + } + + +def test_skipped_values_keep_restamp_counters_but_withhold_fetch_fields() -> None: + values = _sync_state_values( + _base_summary(skipped=True, skipped_reason="refresh_disabled"), + NOW, + ) + # Re-stamp half ran → its counters persist even on a skipped fetch. + assert values["stamped"] == 7 + assert values["cleared"] == 2 + assert values["last_result"] == "skipped" + assert values["skipped_reason"] == "refresh_disabled" + # Fetch-derived fields ABSENT so the UPSERT preserves last-good values. + for withheld in ( + "last_synced_at", + "snapshot", + "snapshot_date", + "products_ok", + "products_failed", + ): + assert withheld not in values + + +# --------------------------------------------------------------------------- +# _snapshot_to_dataset / _newer_dataset +# --------------------------------------------------------------------------- + + +def test_snapshot_to_dataset_roundtrip_and_garbage() -> None: + good = {"_snapshot": "2026-07-01", "express": [{"cycle": "4", "eol": False}]} + dataset = _snapshot_to_dataset(good) + assert dataset is not None + assert dataset.snapshot == "2026-07-01" + assert dataset.cycles("express") + + assert _snapshot_to_dataset(None) is None + assert _snapshot_to_dataset("garbage") is None + assert _snapshot_to_dataset({"express": []}) is None # no _snapshot + assert _snapshot_to_dataset({"_snapshot": "2026-07-01"}) is None # no products + + +@pytest.mark.parametrize( + ("vendored_date", "fetched_date", "winner"), + [ + ("2026-07-01", "2026-07-08", "fetched"), + ("2026-07-08", "2026-07-01", "vendored"), + ("2026-07-08", "2026-07-08", "fetched"), # tie → fetched + ("garbage", "2026-07-01", "fetched"), # unparseable loses + ("2026-07-01", "garbage", "vendored"), + ], +) +def test_newer_dataset_prefers_the_fresher_snapshot( + vendored_date: str, fetched_date: str, winner: str +) -> None: + vendored = EolDataset(snapshot=vendored_date, products={"a": []}) + fetched = EolDataset(snapshot=fetched_date, products={"b": []}) + result = _newer_dataset(vendored, fetched) + assert result is (fetched if winner == "fetched" else vendored) + + +def test_newer_dataset_handles_missing_sides() -> None: + only = EolDataset(snapshot="2026-07-01", products={"a": []}) + assert _newer_dataset(None, only) is only + assert _newer_dataset(only, None) is only + assert _newer_dataset(None, None) is None + + +# --------------------------------------------------------------------------- +# _fetch_half — gating + sanity floor +# --------------------------------------------------------------------------- + + +def test_fetch_half_disabled_by_default(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("EOL_REFRESH_ENABLED", raising=False) + summary = _base_summary(products_ok=0, snapshot=None) + assert _fetch_half(summary) is None + assert summary["skipped"] is True + assert summary["skipped_reason"] == "refresh_disabled" + + +def test_fetch_half_feed_unavailable_skips( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from integrations.eol_feed import EolFeedUnavailable + + monkeypatch.setenv("EOL_REFRESH_ENABLED", "true") + + def _boom(products: list[str]) -> Any: + raise EolFeedUnavailable("all down") + + monkeypatch.setattr("tasks.eol_catalog_refresh.fetch_eol_dataset", _boom) + summary = _base_summary() + assert _fetch_half(summary) is None + assert summary["skipped_reason"] == "feed_unavailable" + + +def test_fetch_half_sanity_floor_rejects_gutted_sweep( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from integrations.eol_feed import EolFetchResult + + monkeypatch.setenv("EOL_REFRESH_ENABLED", "true") + + def _mostly_failed(products: list[str]) -> EolFetchResult: + return EolFetchResult( + dataset={"_snapshot": "2026-07-12", products[0]: []}, + fetched=[products[0]], # 1 of 10 — far below the floor + failed=products[1:], + ) + + monkeypatch.setattr( + "tasks.eol_catalog_refresh.fetch_eol_dataset", _mostly_failed + ) + summary = _base_summary(snapshot=None) + assert _fetch_half(summary) is None + assert summary["skipped_reason"] == "feed_below_sanity_floor" + + +def test_fetch_half_accepts_a_healthy_sweep( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from integrations.eol_feed import EolFetchResult + + monkeypatch.setenv("EOL_REFRESH_ENABLED", "true") + + def _healthy(products: list[str]) -> EolFetchResult: + dataset: dict[str, Any] = {"_snapshot": "2026-07-12"} + for product in products: + dataset[product] = [{"cycle": "1", "eol": False}] + return EolFetchResult(dataset=dataset, fetched=list(products), failed=[]) + + monkeypatch.setattr("tasks.eol_catalog_refresh.fetch_eol_dataset", _healthy) + summary = _base_summary(snapshot=None, products_ok=0) + dataset = _fetch_half(summary) + assert dataset is not None + assert summary["skipped"] is False + assert summary["products_ok"] > 0 + assert summary["snapshot_date"] == "2026-07-12" + + +# --------------------------------------------------------------------------- +# _clear_stamp +# --------------------------------------------------------------------------- + + +class _FakeComponentVersion: + eol_state: str | None = "eol" + eol_product: str | None = "express" + eol_cycle: str | None = "3" + eol_date: date | None = date(2020, 1, 1) + eol_source: str | None = "endoflife.date@2026-01-01" + eol_evaluated_at: datetime | None = None + + +def test_clear_stamp_nulls_everything_once() -> None: + row = _FakeComponentVersion() + assert _clear_stamp(row) is True # type: ignore[arg-type] + assert row.eol_state is None + assert row.eol_product is None + assert row.eol_cycle is None + assert row.eol_date is None + assert row.eol_source is None + assert row.eol_evaluated_at is not None + + stamp_time = row.eol_evaluated_at + assert _clear_stamp(row) is False # type: ignore[arg-type] + assert row.eol_evaluated_at == stamp_time # idempotent — not re-dirtied diff --git a/apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py b/apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py index 973830e4..4c544107 100644 --- a/apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py +++ b/apps/backend/tests/unit/tasks/test_scan_source_cocoapods_merge.py @@ -137,7 +137,8 @@ def test_merge_rewrite_failure_keeps_memory_and_disk_unmerged( from tasks import scan_source monkeypatch.setattr( - scan_source.sbom_scope_filter, "rewrite_sbom_file", lambda path, sbom: False + "tasks.scan_source.sbom_scope_filter.rewrite_sbom_file", + lambda path, sbom: False, ) cdxgen_result = _cdxgen_result(tmp_path) original = copy.deepcopy(cdxgen_result.sbom) diff --git a/apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py b/apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py index 6ee615cf..e4e7b6a9 100644 --- a/apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py +++ b/apps/backend/tests/unit/tasks/test_scan_source_eol_stamp.py @@ -24,7 +24,7 @@ import uuid from datetime import date from pathlib import Path -from typing import Any +from typing import Any, cast import pytest @@ -97,7 +97,7 @@ def fixture_dataset(monkeypatch: pytest.MonkeyPatch) -> None: def _sbom() -> dict[str, Any]: - return json.loads(SBOM_PATH.read_text(encoding="utf-8")) + return cast(dict[str, Any], json.loads(SBOM_PATH.read_text(encoding="utf-8"))) def _persist(session: _FakeSession) -> None: diff --git a/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py index b83b9df6..c77c1018 100644 --- a/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py +++ b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py @@ -24,7 +24,7 @@ import shutil import uuid from pathlib import Path -from typing import Any +from typing import Any, cast import pytest @@ -39,15 +39,18 @@ def _load(path: Path) -> dict[str, Any]: - return json.loads(path.read_text(encoding="utf-8")) + return cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8"))) def _refs(components: list[dict[str, Any]]) -> set[str]: - return { - c.get("bom-ref") or c.get("purl") - for c in components - if isinstance(c, dict) and (c.get("bom-ref") or c.get("purl")) - } + refs: set[str] = set() + for component in components: + if not isinstance(component, dict): + continue + ref = component.get("bom-ref") or component.get("purl") + if isinstance(ref, str) and ref: + refs.add(ref) + return refs # --------------------------------------------------------------------------- @@ -280,7 +283,8 @@ def test_apply_scope_filter_rewrite_failure_keeps_memory_and_disk_unfiltered( from tasks import scan_source monkeypatch.setattr( - scan_source.sbom_scope_filter, "rewrite_sbom_file", lambda path, sbom: False + "tasks.scan_source.sbom_scope_filter.rewrite_sbom_file", + lambda path, sbom: False, ) recorded: list[Any] = [] monkeypatch.setattr( diff --git a/apps/backend/tests/unit/test_catalog_contracts.py b/apps/backend/tests/unit/test_catalog_contracts.py index 3583891e..c77a446f 100644 --- a/apps/backend/tests/unit/test_catalog_contracts.py +++ b/apps/backend/tests/unit/test_catalog_contracts.py @@ -17,6 +17,8 @@ class of defect — the in-app ``notification_kind`` enum and the dispatcher's import typing +from pydantic import BaseModel + # --------------------------------------------------------------------------- # Notification kinds — H-5 guard # --------------------------------------------------------------------------- @@ -272,13 +274,13 @@ def test_eol_states_catalog_matches_schema_literals() -> None: The FE mirror half is ``apps/frontend/tests/unit/contracts/catalogMirrors.test.ts``. """ - from services.eol.eol_catalog import EOL_STATES from schemas.project_detail import ComponentDetailResponse, ComponentSummary + from services.eol.eol_catalog import EOL_STATES expected = {"eol", "supported", "unknown"} assert set(EOL_STATES) == expected - def _literal_states(model: type, field: str) -> set[str]: + def _literal_states(model: type[BaseModel], field: str) -> set[str]: annotation = model.model_fields[field].annotation # ``Literal["eol","supported","unknown"] | None`` — walk the union. states: set[str] = set() diff --git a/apps/frontend/src/features/admin/health/AdminHealthPage.tsx b/apps/frontend/src/features/admin/health/AdminHealthPage.tsx index c3660db5..2fcd447c 100644 --- a/apps/frontend/src/features/admin/health/AdminHealthPage.tsx +++ b/apps/frontend/src/features/admin/health/AdminHealthPage.tsx @@ -20,6 +20,7 @@ import { type HealthComponent, type HealthStatus, } from "@/features/admin/health/api/adminHealthApi"; +import { EolPanel } from "@/features/admin/health/EolPanel"; import { KevFeedPanel } from "@/features/admin/health/KevFeedPanel"; import { TrivyDBPanel } from "@/features/admin/health/TrivyDBPanel"; import { useAdminHealth } from "@/features/admin/health/api/useAdminHealth"; @@ -162,6 +163,7 @@ export function AdminHealthPage() {
+
+

{label}

+

+ {value} +

+
+ ); +} + +interface EolPanelProps { + /** Optional clock override for deterministic unit tests. */ + now?: number; +} + +export function EolPanel({ now }: EolPanelProps = {}) { + const { t, i18n } = useTranslation("admin"); + const query = useAdminEolHealth(); + const locale = i18n.resolvedLanguage; + const clock = now ?? Date.now(); + + const renderHeading = (badge?: ReactNode) => ( +
+

+ + {t("admin.eol.heading")} +

+
+ {query.isFetching ? ( + + ) : null} + {badge} +
+
+ ); + + if (query.isLoading) { + return ( +
+ {renderHeading()} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+
+ ); + } + + if (query.isError) { + return ( +
+ {renderHeading()} + + + {t(adminErrorMessageKey(query.error))} + + +
+ ); + } + + const data: EolStatus | undefined = query.data; + if (!data) { + return null; + } + + const ageDays = snapshotAgeDays(data.snapshot_date, clock); + const isStale = ageDays != null && ageDays > STALE_AFTER_DAYS; + + const status: PanelStatus = !data.enabled + ? "disabled" + : isStale + ? "stale" + : data.last_result === "skipped" + ? "skipped" + : "ok"; + + const visuals = statusVisuals(status); + const StatusIcon = visuals.icon; + const badge = ( + + + {t(`admin.eol.status.${status}`)} + + ); + + return ( +
+ {renderHeading(badge)} + + {status === "stale" ? ( +

+ + {t("admin.eol.stale_note", { days: ageDays })} +

+ ) : null} + {status === "skipped" && data.skipped_reason ? ( +

+ + {t("admin.eol.skipped_reason", { reason: data.skipped_reason })} +

+ ) : null} + +
+ + + + +
+ +
+

+ + {t("admin.eol.footer.origin")}: + {" "} + {data.snapshot_origin + ? t(`admin.eol.origin.${data.snapshot_origin}`) + : DASH} + {" · "} + + {t("admin.eol.footer.products")}: + {" "} + {formatCount(data.product_count, locale)} +

+

+ + {t("admin.eol.footer.refresh")}: + {" "} + {data.refresh_enabled + ? t("admin.eol.footer.refresh_on", { host: data.feed_host ?? DASH }) + : t("admin.eol.footer.refresh_off")} +

+
+
+ ); +} diff --git a/apps/frontend/src/features/admin/health/api/adminEolHealthApi.ts b/apps/frontend/src/features/admin/health/api/adminEolHealthApi.ts new file mode 100644 index 00000000..3eaa22c4 --- /dev/null +++ b/apps/frontend/src/features/admin/health/api/adminEolHealthApi.ts @@ -0,0 +1,41 @@ +/** + * Admin endoflife.date snapshot health REST surface — Phase M / PR M-3. + * + * Mirrors the backend contract: + * GET /v1/admin/eol/health → EolStatusOut + * + * Kept separate from the Trivy / KEV clients (same rationale: independent + * evolution). Unlike the KEV panel there is no strict "never ran" empty + * state — the vendored snapshot ships with every release, so the + * config-derived fields (`snapshot_date`, `snapshot_origin`, `rule_count`, + * `product_count`) are populated even before the first beat tick; only the + * beat-derived fields are null then. + */ +import { api } from "@/lib/api"; + +export type EolSyncResult = "synced" | "skipped"; +export type EolSnapshotOrigin = "vendored" | "feed"; + +export interface EolStatus { + enabled: boolean; + refresh_enabled: boolean; + snapshot_date: string | null; + snapshot_origin: EolSnapshotOrigin | null; + rule_count: number; + product_count: number; + eol_flagged_total: number | null; + last_synced_at: string | null; + last_attempt_at: string | null; + last_result: EolSyncResult | null; + skipped_reason: string | null; + stamped: number | null; + cleared: number | null; + duration_ms: number | null; + next_refresh_at: string | null; + feed_host: string | null; +} + +export async function getAdminEolHealth(): Promise { + const { data } = await api.get("/v1/admin/eol/health"); + return data; +} diff --git a/apps/frontend/src/features/admin/health/api/useAdminEolHealth.ts b/apps/frontend/src/features/admin/health/api/useAdminEolHealth.ts new file mode 100644 index 00000000..b84d0ea8 --- /dev/null +++ b/apps/frontend/src/features/admin/health/api/useAdminEolHealth.ts @@ -0,0 +1,27 @@ +/** + * TanStack Query hook for the admin EOL snapshot panel — Phase M / PR M-3. + * + * Polls every 60s, matching the sibling Trivy / KEV panels: the EOL beat is + * weekly, so the poll exists only to keep the panel honest after a manual + * task run without a page reload. + */ +import { useQuery, type UseQueryResult } from "@tanstack/react-query"; + +import { + getAdminEolHealth, + type EolStatus, +} from "@/features/admin/health/api/adminEolHealthApi"; + +export function adminEolHealthQueryKey() { + return ["admin", "eol", "health"] as const; +} + +export function useAdminEolHealth(options?: { + refetchIntervalMs?: number | false; +}): UseQueryResult { + return useQuery({ + queryKey: adminEolHealthQueryKey(), + queryFn: () => getAdminEolHealth(), + refetchInterval: options?.refetchIntervalMs ?? 60_000, + }); +} diff --git a/apps/frontend/src/locales/en/admin.json b/apps/frontend/src/locales/en/admin.json index e135d9e0..2a8b4a83 100644 --- a/apps/frontend/src/locales/en/admin.json +++ b/apps/frontend/src/locales/en/admin.json @@ -328,6 +328,35 @@ "feed_host": "Feed host" } }, + "eol": { + "heading": "endoflife.date snapshot", + "status": { + "ok": "OK", + "stale": "Stale", + "skipped": "Skipped", + "disabled": "Disabled" + }, + "stale_note": "The snapshot is {{days}} days old — upgrade the release or refresh it with scripts/refresh_eol_snapshot.py.", + "skipped_reason": "Last fetch skipped: {{reason}}", + "kpi": { + "snapshot_date": "Snapshot date", + "snapshot_age": "{{days}} days old", + "flagged_total": "EOL-flagged components", + "stamped_cleared": "Recently stamped / cleared", + "next_refresh": "Next tick" + }, + "origin": { + "vendored": "vendored with the release", + "feed": "fetched from the feed" + }, + "footer": { + "origin": "Dataset", + "products": "Products", + "refresh": "Live refresh", + "refresh_on": "on ({{host}})", + "refresh_off": "off — vendored snapshot only" + } + }, "backup": { "title": "Backups", "subtitle": "Automated daily backups + manual triggers + restore.", diff --git a/apps/frontend/src/locales/ko/admin.json b/apps/frontend/src/locales/ko/admin.json index f8d4998d..d763e2c3 100644 --- a/apps/frontend/src/locales/ko/admin.json +++ b/apps/frontend/src/locales/ko/admin.json @@ -328,6 +328,35 @@ "feed_host": "피드 호스트" } }, + "eol": { + "heading": "endoflife.date 스냅숏", + "status": { + "ok": "정상", + "stale": "오래됨", + "skipped": "건너뜀", + "disabled": "비활성" + }, + "stale_note": "스냅숏이 {{days}}일 지났습니다 — 릴리즈를 업그레이드하거나 scripts/refresh_eol_snapshot.py 로 갱신하십시오.", + "skipped_reason": "마지막 갱신 건너뜀: {{reason}}", + "kpi": { + "snapshot_date": "스냅숏 날짜", + "snapshot_age": "{{days}}일 경과", + "flagged_total": "지원 종료 컴포넌트", + "stamped_cleared": "최근 기록 / 해제", + "next_refresh": "다음 실행" + }, + "origin": { + "vendored": "릴리즈에 벤더링됨", + "feed": "피드에서 수집됨" + }, + "footer": { + "origin": "데이터셋", + "products": "제품 수", + "refresh": "실시간 갱신", + "refresh_on": "켬 ({{host}})", + "refresh_off": "끔 — 벤더 스냅숏만 사용" + } + }, "backup": { "title": "백업", "subtitle": "자동 일일 백업 + 수동 백업 트리거 + 복원.", diff --git a/apps/frontend/tests/unit/features/admin/health/EolPanel.test.tsx b/apps/frontend/tests/unit/features/admin/health/EolPanel.test.tsx new file mode 100644 index 00000000..23bfab2f --- /dev/null +++ b/apps/frontend/tests/unit/features/admin/health/EolPanel.test.tsx @@ -0,0 +1,190 @@ +/** + * EolPanel — unit tests (Phase M / PR M-3). + * + * Coverage targets (mirrors KevFeedPanel.test.tsx): + * - OK state renders 4 KPI tiles + emerald badge + origin/refresh footer, + * raw wire values on the `data-value` e2e anchors. + * - Stale snapshot (>180 days) escalates to the amber "stale" badge + the + * stale note with the day count. + * - Skipped fetch renders the amber badge + the raw skip reason. + * - Disabled flagging renders the muted badge (outranks everything). + * - Beat-never-ran keeps the KPI grid (vendored snapshot side is always + * populated) with dashes for the beat-derived tiles — no EmptyState. + * - Loading skeletons + page-level error alert. + */ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { EolPanel } from "@/features/admin/health/EolPanel"; + +vi.mock("@/features/admin/health/api/adminEolHealthApi", async () => { + return { + getAdminEolHealth: vi.fn(), + }; +}); + +import { + getAdminEolHealth, + type EolStatus, +} from "@/features/admin/health/api/adminEolHealthApi"; + +const mockedGet = vi.mocked(getAdminEolHealth); + +const REFERENCE_NOW = new Date("2026-07-12T09:00:00Z").getTime(); + +function statusFixture(overrides: Partial = {}): EolStatus { + return { + enabled: true, + refresh_enabled: false, + snapshot_date: "2026-07-11", + snapshot_origin: "vendored", + rule_count: 11, + product_count: 10, + eol_flagged_total: 4, + last_synced_at: null, + last_attempt_at: "2026-07-12T02:15:00Z", + last_result: "skipped", + skipped_reason: null, + stamped: 7, + cleared: 1, + duration_ms: 900, + next_refresh_at: "2026-07-19T02:15:00Z", + feed_host: "endoflife.date", + ...overrides, + }; +} + +function renderPanel(now: number = REFERENCE_NOW) { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return render( + + + , + ); +} + +describe("EolPanel", () => { + beforeEach(() => { + mockedGet.mockReset(); + }); + + it("renders the OK state with KPI grid and emerald badge", async () => { + mockedGet.mockResolvedValue( + statusFixture({ last_result: "synced", last_synced_at: "2026-07-12T02:15:05Z" }), + ); + renderPanel(); + + await waitFor(() => + expect(screen.getByTestId("eol-panel")).toHaveAttribute("data-status", "ok"), + ); + expect(screen.getByTestId("eol-status-badge")).toHaveAttribute( + "data-status", + "ok", + ); + expect(screen.getByTestId("eol-kpi-snapshot-date")).toHaveAttribute( + "data-value", + "2026-07-11", + ); + expect(screen.getByTestId("eol-kpi-flagged-total")).toHaveAttribute( + "data-value", + "4", + ); + expect(screen.getByTestId("eol-kpi-stamped-cleared")).toHaveAttribute( + "data-value", + "7/1", + ); + expect(screen.getByTestId("eol-kpi-next-refresh")).toHaveAttribute( + "data-value", + "2026-07-19T02:15:00Z", + ); + expect(screen.getByTestId("eol-footer")).toBeInTheDocument(); + }); + + it("escalates a >180-day-old snapshot to the stale badge + note", async () => { + mockedGet.mockResolvedValue( + statusFixture({ snapshot_date: "2025-06-01", last_result: "synced" }), + ); + renderPanel(); + + await waitFor(() => + expect(screen.getByTestId("eol-panel")).toHaveAttribute( + "data-status", + "stale", + ), + ); + expect(screen.getByTestId("eol-stale-note")).toBeInTheDocument(); + }); + + it("renders the skipped state with the raw reason line", async () => { + mockedGet.mockResolvedValue( + statusFixture({ last_result: "skipped", skipped_reason: "feed_unavailable" }), + ); + renderPanel(); + + await waitFor(() => + expect(screen.getByTestId("eol-panel")).toHaveAttribute( + "data-status", + "skipped", + ), + ); + expect(screen.getByTestId("eol-skipped-reason")).toHaveTextContent( + "feed_unavailable", + ); + }); + + it("disabled flagging outranks every other status", async () => { + mockedGet.mockResolvedValue( + statusFixture({ enabled: false, snapshot_date: "2025-01-01" }), + ); + renderPanel(); + + await waitFor(() => + expect(screen.getByTestId("eol-panel")).toHaveAttribute( + "data-status", + "disabled", + ), + ); + }); + + it("beat-never-ran keeps the grid with dashes (no EmptyState)", async () => { + mockedGet.mockResolvedValue( + statusFixture({ + last_attempt_at: null, + last_result: null, + stamped: null, + cleared: null, + duration_ms: null, + }), + ); + renderPanel(); + + await waitFor(() => + expect(screen.getByTestId("eol-panel")).toHaveAttribute("data-status", "ok"), + ); + // Snapshot side still populated; beat side dashes. + expect(screen.getByTestId("eol-kpi-snapshot-date")).toHaveAttribute( + "data-value", + "2026-07-11", + ); + expect(screen.getByTestId("eol-kpi-stamped-cleared")).not.toHaveAttribute( + "data-value", + ); + }); + + it("shows loading skeletons before the query resolves", () => { + mockedGet.mockReturnValue(new Promise(() => {}) as Promise); + renderPanel(); + expect(screen.getAllByTestId("eol-skeleton")).toHaveLength(4); + }); + + it("renders the error alert when the query rejects", async () => { + mockedGet.mockRejectedValue(new Error("boom")); + renderPanel(); + await waitFor(() => + expect(screen.getByTestId("eol-error")).toBeInTheDocument(), + ); + }); +}); diff --git a/docs-site/docs/admin-guide/disk-and-health.md b/docs-site/docs/admin-guide/disk-and-health.md index f35ecc98..3b5b1bd7 100644 --- a/docs-site/docs/admin-guide/disk-and-health.md +++ b/docs-site/docs/admin-guide/disk-and-health.md @@ -51,6 +51,31 @@ Each row maps to a real probe in `services/admin_health_service.py`: The portal does not separately probe `backend`, `worker`, `beat`, `frontend`, or `traefik`. Their liveness is implicit: if the dashboard renders at all, the backend is up; if the `celery` row is `ok`, the worker (and the broker the worker depends on) are reachable. +### endoflife.date snapshot panel {#eol-panel} + + +Below the Trivy DB and KEV feed panels, the **endoflife.date snapshot** panel +tracks the dataset behind the Components tab's +[EOL badge](../user-guide/components-and-licenses.md#end-of-life-flagging): + +- **Snapshot date** — the effective dataset's build date (the newer of the + release-vendored snapshot and, when `EOL_REFRESH_ENABLED=true`, the last + fetched one). The panel escalates to an amber **Stale** badge past 180 + days — upgrade the release or rebuild the snapshot with + `python3 scripts/refresh_eol_snapshot.py`. +- **EOL-flagged components** — live count of catalog component versions past + their end-of-life. +- **Recently stamped / cleared** — what the weekly beat's re-stamp pass did + on its last tick. The re-stamp runs even when the live fetch is off — it + is a pure-local pass that applies a newer vendored snapshot to existing + rows and clears stamps the whitelist no longer covers. +- **Next tick** — derived from the live Celery beat schedule (Sunday 02:15 + UTC by default). + +The footer shows the dataset origin (vendored vs fetched) and whether the +live refresh is on. The refresh is **off by default** — see +`EOL_REFRESH_ENABLED` in [Environment variables](../reference/env-variables.md). + ## Disk dashboard {#disk} diff --git a/docs-site/docs/reference/env-variables.md b/docs-site/docs/reference/env-variables.md index ae09d3af..0c0597f1 100644 --- a/docs-site/docs/reference/env-variables.md +++ b/docs-site/docs/reference/env-variables.md @@ -124,6 +124,9 @@ See [build gate](./glossary.md#build-gates) for the gate model and [Gate the bui | `SCAN_SCOPE_FILTER_NODE_ENABLED` | `true` | `config.py` | npm half of the scope filter (drops packages the committed/generated `package-lock.json` classifies as `dev`). A package absent from the lockfile is always kept. | | `EOL_ENABLED` | `true` | `config.py` | End-of-life flagging: components matching the endoflife.date product whitelist are stamped `eol` / `supported` / `unknown` on the shared catalog. Fully offline — verdicts come from a snapshot vendored with the release, zero egress. Only `false` / `0` / `no` disable. See [Components & licenses → End-of-life flagging](../user-guide/components-and-licenses.md#end-of-life-flagging). | | `EOL_SNAPSHOT_PATH` | *(empty — vendored file)* | `config.py` | Operator override for the endoflife.date snapshot. Air-gapped installs can build a fresher snapshot on a connected host (`python3 scripts/refresh_eol_snapshot.py`), mount it, and point this at the file. | +| `EOL_REFRESH_ENABLED` | `false` | `config.py` | Opt-in live fetch: the weekly beat downloads fresh lifecycle data from `EOL_FEED_URL_TEMPLATE`. **Off by default** — this is new egress to a third-party host; the beat's local re-stamp pass runs either way. Only the exact tokens `true` / `1` / `yes` enable (fail-closed, the SCANOSS posture). | +| `EOL_FEED_URL_TEMPLATE` | `https://endoflife.date/api/{product}.json` | `config.py` | Per-product API template for the live fetch (`{product}` is substituted). Point at an internal mirror to keep the egress inside your network. | +| `EOL_REFRESH_TIMEOUT_SECONDS` | `15` | `config.py` | HTTP timeout per product request during the live fetch. Bounded `[1, 120]`; the whole sweep is additionally capped at 60 s wall-clock. | | `WORKSPACE_HOST_PATH` | `/tmp/trustedoss` | `config.py`, `docker-compose.yml` | Host directory mounted into the worker as `/workspace`. Holds repo clones + scan artefacts (cdxgen SBOM, scancode output). The compose stack overrides this to `/workspace` inside the container. | | `ORT_RULES_PATH` | `/opt/trustedoss/ort/rules.kts` | `docker-compose.yml` | Legacy path inside the worker, vestigial after the ORT stage was removed. The file is a placeholder and has no effect in this release — license-tier classification comes from `_LICENSE_CATEGORY_DEFAULTS` in `apps/backend/tasks/scan_source.py`. | | `JSONB_ROW_SIZE_LIMIT_BYTES` | `262144` (256 KB) | `config.py` | Per-row JSON byte ceiling before the writer truncates and emits a warning. Guards the I-1 unbounded-payload class. | diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/admin-guide/disk-and-health.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/admin-guide/disk-and-health.md index 558c2d90..d3d40673 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/admin-guide/disk-and-health.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/admin-guide/disk-and-health.md @@ -51,6 +51,29 @@ sidebar_position: 3 포털은 `backend`, `worker`, `beat`, `frontend`, `traefik`을 별도 프로브하지 않습니다. 이들의 liveness 는 암묵적입니다 — 대시보드가 렌더되면 backend 가동 중, `celery` 행이 `ok`이면 worker(와 worker 가 의존하는 broker)도 도달 가능. +### endoflife.date 스냅숏 패널 {#eol-panel} + + +Trivy DB·KEV 피드 패널 아래의 **endoflife.date 스냅숏** 패널은 Components 탭 +[EOL 배지](../user-guide/components-and-licenses.md#end-of-life-flagging)의 +데이터셋 상태를 보여줍니다. + +- **스냅숏 날짜** — 유효 데이터셋의 생성일(릴리즈에 벤더링된 스냅숏과, + `EOL_REFRESH_ENABLED=true` 인 경우 마지막으로 수집한 스냅숏 중 더 최신). + 180일이 지나면 주황색 **오래됨** 배지로 격상됩니다 — 릴리즈를 + 업그레이드하거나 `python3 scripts/refresh_eol_snapshot.py` 로 스냅숏을 + 다시 만드십시오. +- **지원 종료 컴포넌트** — 지원 종료를 지난 카탈로그 컴포넌트 버전의 실시간 + 개수. +- **최근 기록 / 해제** — 주간 beat 의 재기록 패스가 마지막 실행에서 한 일. + 재기록은 실시간 수집이 꺼져 있어도 실행됩니다 — 순수 로컬 패스로, 더 새 + 벤더 스냅숏을 기존 행에 반영하고 목록에서 빠진 제품의 기록을 지웁니다. +- **다음 실행** — 라이브 Celery beat 스케줄에서 유도(기본 일요일 02:15 UTC). + +푸터에는 데이터셋 출처(벤더링/피드 수집)와 실시간 갱신 여부가 표시됩니다. +실시간 갱신은 **기본 꺼짐**입니다 — [환경변수](../reference/env-variables.md)의 +`EOL_REFRESH_ENABLED` 를 참고하십시오. + ## 디스크 대시보드 {#disk} diff --git a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md index 65825d0f..43be8adf 100644 --- a/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md +++ b/docs-site/i18n/ko/docusaurus-plugin-content-docs/current/reference/env-variables.md @@ -123,6 +123,9 @@ CI 빌드 게이트는 기본적으로 Critical CVE와 금지 라이선스에서 | `SCAN_SCOPE_FILTER_NODE_ENABLED` | `true` | `config.py` | 스코프 필터의 npm 부분(커밋되었거나 prep 단계가 생성한 `package-lock.json` 이 `dev` 로 분류한 패키지 제거). lockfile 에 없는 패키지는 항상 유지합니다. | | `EOL_ENABLED` | `true` | `config.py` | 지원 종료(EOL) 표시: endoflife.date 추적 제품 목록에 맞는 컴포넌트를 공유 카탈로그에 `eol` / `supported` / `unknown` 으로 기록합니다. 완전 오프라인 — 판정은 릴리즈에 벤더링된 스냅숏에서 나오며 외부 전송이 없습니다. 정확히 `false` / `0` / `no` 만 끕니다. [컴포넌트·라이선스 → 지원 종료 표시](../user-guide/components-and-licenses.md#end-of-life-flagging) 참고. | | `EOL_SNAPSHOT_PATH` | *(빈 값 — 벤더 파일)* | `config.py` | endoflife.date 스냅숏 재정의. air-gapped 설치에서는 연결된 호스트에서 더 신선한 스냅숏을 만들어(`python3 scripts/refresh_eol_snapshot.py`) 마운트한 뒤 이 변수로 지정합니다. | +| `EOL_REFRESH_ENABLED` | `false` | `config.py` | 실시간 수집 opt-in: 주간 beat 가 `EOL_FEED_URL_TEMPLATE` 에서 신선한 라이프사이클 데이터를 내려받습니다. **기본 꺼짐** — 새로운 외부 전송이기 때문입니다. beat 의 로컬 재기록 패스는 이 값과 무관하게 실행됩니다. 정확히 `true` / `1` / `yes` 토큰만 켭니다(fail-closed, SCANOSS 방식). | +| `EOL_FEED_URL_TEMPLATE` | `https://endoflife.date/api/{product}.json` | `config.py` | 실시간 수집용 제품별 API 템플릿(`{product}` 치환). 내부 미러를 지정하면 전송이 사내에 머뭅니다. | +| `EOL_REFRESH_TIMEOUT_SECONDS` | `15` | `config.py` | 실시간 수집 시 제품 요청당 HTTP 타임아웃. `[1, 120]` 범위이며, 전체 수집은 별도로 60초 wall-clock 으로 제한됩니다. | | `WORKSPACE_HOST_PATH` | `/tmp/trustedoss` | `config.py`, `docker-compose.yml` | worker에 `/workspace`로 마운트되는 호스트 디렉터리. 레포 클론 + 스캔 아티팩트(cdxgen SBOM, scancode 출력) 보관. compose 스택은 컨테이너 내에서 `/workspace`로 오버라이드합니다. | | `ORT_RULES_PATH` | `/opt/trustedoss/ort/rules.kts` | `docker-compose.yml` | worker 내부 레거시 경로로, ORT 단계 제거 후 잔재입니다. 파일은 placeholder 이며 v0.10.0 에서는 효과가 없습니다 — 라이선스 단계 분류는 `apps/backend/tasks/scan_source.py` 의 `_LICENSE_CATEGORY_DEFAULTS` 에서 옵니다. | | `JSONB_ROW_SIZE_LIMIT_BYTES` | `262144` (256 KB) | `config.py` | writer가 truncate + warn하기 전 행당 JSON 바이트 상한. I-1 무한 페이로드 클래스 가드. | From d530ce945d642abfc005f5ea450402881c4cc6a0 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 01:42:28 +0900 Subject: [PATCH 07/10] docs(parity): close gaps #15-#17 (Phase K/L/M) and move the baseline to 5a62094 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7th review cycle over BomLens 9cc37e0..5a62094 (v1.7.0 + 27 commits, 80 total): the runtime-scope filter series, EOL flagging and the iOS lockfile work were judged as gaps #15-#17 and implemented in this same branch — all closed. Baseline, local-path correction (~/projects/bomlens) and the review history row updated. Follow-up backlog recorded: Android release-classpath scope filter (needs an Android SDK in the worker image), surfacing Trivy's image-level eosl flag for container scans. Also carries two doc changes that were finished earlier but stranded (docs-only PRs are BLOCKED by the ci.yml paths-ignore required checks): the reachability-analysis coordination row and docs/commercial-parity-candidates.md. --- docs/bomlens-parity-review.md | 13 +++-- docs/commercial-parity-candidates.md | 79 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 docs/commercial-parity-candidates.md diff --git a/docs/bomlens-parity-review.md b/docs/bomlens-parity-review.md index a41b301f..014ce2e1 100644 --- a/docs/bomlens-parity-review.md +++ b/docs/bomlens-parity-review.md @@ -1,6 +1,6 @@ # BomLens 격차 추적 (Parity Review) -자매 프로젝트 BomLens(`sktelecom/sbom-tools`, 로컬 경로 `~/projects/sbom-tools`)의 개선을 주기적으로 확인하고 TRUSCA 관점의 격차를 추적하는 살아있는 문서다. BomLens는 릴리즈 주기가 매우 짧으므로(2026-06 기준 3주에 6개 릴리즈) 1회성 분석 대신 이 문서를 갱신한다. +자매 프로젝트 BomLens(`sktelecom/sbom-tools`, 로컬 경로 `~/projects/bomlens`)의 개선을 주기적으로 확인하고 TRUSCA 관점의 격차를 추적하는 살아있는 문서다. BomLens는 릴리즈 주기가 매우 짧으므로(2026-06 기준 3주에 6개 릴리즈) 1회성 분석 대신 이 문서를 갱신한다. ## 리뷰 절차 @@ -13,9 +13,9 @@ | 항목 | 값 | |---|---| -| BomLens 커밋 | `9cc37e0` | -| BomLens 태그 | v1.6.0 (2026-07-03) | -| 리뷰 일자 | 2026-07-04 (6차 — Phase J 종료 후) | +| BomLens 커밋 | `5a62094` | +| BomLens 태그 | v1.7.0 + 27커밋 (2026-07-10) | +| 리뷰 일자 | 2026-07-11 (7차 — Phase K·L·M 완료 후) | ## 격차 표 @@ -37,6 +37,9 @@ | 12 | 릴리즈 게이트 강화(published 이미지에서 first-scan 실증 후 publish) | P3 | **closed (#457, 2026-07-03 Phase I)** — v0.13.0 실검증 대기 | BomLens #239/#241 draft→검증→publish 미러. release.yml에 `release`(draft GitHub Release) + `release-gate`(발행 이미지 pull·compose 부팅·quickstart 스모크→`gh release edit --draft=false`) 잡, `docker-compose.smoke.yml` 오버레이, 기여자 가이드 releasing.md(EN/KO). semgrep run-shell-injection 회피(env 간접). 코드 머지 완료 — 게이트 실동작은 다음 실릴리즈 v0.13.0 태그에서 최종 확인 | | 13 | 바이너리/펌웨어/ROOTFS 스캔 | — | 역할경계 | BomLens 영역. "BomLens로 스캔 → TRUSCA ingest" 연계 문서화로 커버 권장 | | 14 | 데스크톱 앱, local-first, `--byte-stable`·cosign 서명 | — | 역할경계 | BomLens 정체성 영역 | +| 15 | 런타임 스코프 필터(test/dev 의존성 SBOM 제외) | P1 | **closed (Phase K, 2026-07-11)** | BomLens #331/#335/#337/#341 미러. cdxgen 이 test/provided/dev 의존성까지 담아 CVE·의무 과대 집계 — TRUSCA 동일 결함 확인. `integrations/sbom_scope_filter.py`: Maven 은 cdxgen scope 태그(hasScopes 가드), Node 는 **BomLens 와 달리 npm 재해석 서브프로세스 없이** 기존 npm_lockfile 분류로(dev 만 제거, keep-if-unknown). persist·서명·Trivy 전에 copy-then-commit 으로 적용, 인제스트 경로는 의도적 무필터(업로드 SBOM = 공급자 선언 진실). 기본 켬(`SCAN_SCOPE_FILTER_*`, 정확한 falsy 토큰만 끔). FE 요약 밴드 "N건 제외" 표시. **Android(release runtime classpath)는 워커에 Android SDK 부재로 후속 백로그** — gradle 유래 컴포넌트는 scope 미탑재라 hasScopes 가드로 무회귀 | +| 16 | EOL(endoflife.date) 컴포넌트 플래그 | P2 | **closed (Phase M, 2026-07-11)** | BomLens #368(enrich-eol.sh) 미러. purl 맵 **원본 그대로 벤더링**(바이트 동일 계약 테스트, %40 정규화는 매처에서 흡수) + 스냅숏은 빌드타임 베이크 대신 **저장소 벤더링**(scripts/refresh_eol_snapshot.py, 릴리즈마다) — Docker 빌드 무네트워크·air-gap 기본 동작. 판정은 component_versions 카탈로그 컬럼(0038, KEV 동형)에 저장, persist 훅에서 스탬프(소스+인제스트 공통). FE: EolBadge(KevBadge 미러)·컬럼·`?eol=true` 필터·드로어 행·Overview 칩. 운영(0039): 주간 재스탬프 beat(항상 로컬 실행) + opt-in 실시간 수집(`EOL_REFRESH_ENABLED` 기본 꺼짐, 위생 플로어) + admin/health 스냅숏 패널(180일 stale 경고) | +| 17 | iOS CocoaPods/SPM lockfile 스캔 | P2 | **closed (Phase L, 2026-07-11)** | BomLens #347 미러. Podfile 존재 + pod CLI 부재 시 cdxgen cocoapods 수집기가 TypeError 로 스캔 전체를 죽이던 크래시 → `--exclude-type cocoapods`(어댑터+사이드카, depth-3 제한 글롭) + `integrations/cocoapods_lockfile.py` 가 Podfile.lock PODS: 블록에서 컴포넌트·그래프 오프라인 복원(**syft 불요** — BomLens 와 분기, npm_lockfile 규율). Package.resolved 만 커밋한 저장소도 swift 감지, 사이드카는 커밋된 lockfile 존재 시 `swift package resolve` 스킵. cdxgen 12.3.3 의 Package.resolved 오프라인 파싱 실측 검증(채록 픽스처가 그 증거) | ## TRUSCA 우위 (참고) @@ -51,6 +54,7 @@ BomLens 내부 방향 문서(`bomlens-internal/identity-and-direction.md`)가 | 2026 9~10월 | 검출 모드 `DETECTION=build\|lightweight\|static` (설계: sbom-tools #300) | 빌드 없는 경량 스캔 — TRUSCA 스캔 파이프라인에도 유효한 옵션 | | 2026 11~12월 | 취약점 변화 추적(재스캔 diff, 로컬 파일 기반) | 격차 표 #6과 같은 영역 — 포털이 먼저 완성하면 역할 경계가 명확해짐 | | 2026 11~12월 | TRUSCA SBOM ingest 연계(업로드 편의) | TRUSCA 쪽 수용 준비 필요 — 특히 ML-BOM 1.7 수용(#2) | +| 협의 대기 | 다언어 도달성 분석(Java/JS 등 call-graph 산출) | 상용(Black Duck) 최대 격차. BomLens가 산출·TRUSCA가 소비하는 공동 후보 — [`commercial-parity-candidates.md`](./commercial-parity-candidates.md) C4. 공용 포맷·역할 경계 선협의 필요 | ## 리뷰 이력 @@ -63,3 +67,4 @@ BomLens 내부 방향 문서(`bomlens-internal/identity-and-direction.md`)가 | 2026-07-03 (5차) | `17d6e59` → `2591230` | Phase E 착수 전 리뷰. v1.6.0 태그 신설이나 기준점 이후 실질 변경은 릴리즈 chore 1건뿐(데스크톱·G7·스캐너 수정 묶음, 전부 기존 격차/역할경계 범위) — 새 격차 없음. #7 in-progress | | 2026-07-03 (완료 스윕) | (기준점 유지 `2591230`) | Phase E~I 연속 완료: #7 카탈로그 확장(#451)·#6 diff e2e(#452)·#8 Excel(#454)·#9 의존성 그래프(#455)·#10 전역 검색(#456)·#12 릴리즈 게이트(#457). 부수: jsonata CVE-2026-52746 .trivyignore 억제(#453, 저장소 전역 image-scan 언블록). **open 격차는 #11(SCANOSS, 조건부 Phase J)만 남음.** #12는 코드 머지 완료, v0.13.0 태그에서 게이트 실동작 최종 확인 예정 | | 2026-07-04 (6차) | `2591230` → `9cc37e0` | Phase J 종료 후 리뷰. 기준점 이후 신규는 #329(firmware CVE DB 게이팅)뿐 — 펌웨어는 역할경계(#13), 새 격차 없음. **#11 SCANOSS closed(#459) — 마지막 격차 마감. 격차 표 P1~P3 실질 전부 closed, open 없음(역할경계 2건 제외).** Phase J 부수로 워커 이미지 linux-libc-dev 커널헤더 제거(Trivy DB 갱신發 76 CVE 영구 해소, #453과 같은 클래스지만 억제 대신 durable 제거) | +| 2026-07-11 (7차) | `9cc37e0` → `5a62094` | v1.7.0 릴리즈 + 후속 27커밋, 총 80커밋 리뷰. 스캔 정밀도 연작이 핵심: 런타임 스코프 필터(#331/#335/#337/#341)·EOL 플래그(#368)·iOS lockfile(#347) — 신규 격차 3건(#15~#17) 판정 즉시 Phase K·L·M 으로 전부 구현·closed. 격차 아님: 펌웨어 CPE 인덱스(#330, 역할경계 #13), Trivy 0.72 범프(TRUSCA 기보유), Windows/문서 인프라. 부수 확인: BomLens 웹 UI 에 **TRUSCA 업로드 기능**(#336) 탑재 — 격차가 아니라 연계 실동작 시작. 후속 백로그: Android release-classpath 스코프 필터(워커 Android SDK 탑재 시), 컨테이너 스캔의 Trivy `eosl`(이미지 OS 단위) 표면화 | diff --git a/docs/commercial-parity-candidates.md b/docs/commercial-parity-candidates.md new file mode 100644 index 00000000..d9d9e7cc --- /dev/null +++ b/docs/commercial-parity-candidates.md @@ -0,0 +1,79 @@ +# 상용 제품 격차 후보 (Commercial Parity Candidates) + +상용 오픈소스 관리 제품(Black Duck, 엘에스웨어 포세라 위드 블랙덕)의 공개 기능을 TRUSCA 현재 구현과 대조해, 도입 가치가 있는 격차를 후보로 추적하는 문서다. 자매 오픈소스 BomLens 기준 격차는 [`bomlens-parity-review.md`](./bomlens-parity-review.md)가 따로 다룬다 — 이 문서는 상용 기준이다. + +출처: 보안뉴스 ISEC 2026 미리보기(2026-07-08, `idx=144551`), 엘에스웨어 포세라 위드 블랙덕 기능 고도화 보도(2026-07), Black Duck SCA 2026.1 제품 자료. + +## 판정 원칙 + +1. TRUSCA 역할(영속 거버넌스 포털)에 맞는 격차만 후보로 채택한다. 아티팩트 저장소·호스트 런타임 에이전트로의 확장은 역할 경계로 제외한다(포세라의 "오픈소스 저장소 운영", "서버 운영단계 상시 점검"이 여기 해당). +2. 이미 구현된 기능은 후보가 아니다. 대조 결과 포세라/Black Duck 헤드라인 기능 다수(CVSS/EPSS/KEV 우선순위, ML-BOM 수용, 다포맷 SBOM 수출, CI/CD 자동점검·빌드 차단)는 TRUSCA가 이미 보유한다. + +## 대조 요약 (2026-07-09) + +| 상용 기능 | TRUSCA 현황 | 판정 | +|---|---|---| +| SBOM 관리(SPDX/CycloneDX 다버전) | ingest·변환·수출 4포맷, 서명, 적합성 검사 | 보유 | +| 라이선스·취약점 점검 | 카탈로그 52종 + Trivy 매칭 | 보유 | +| CVSS + EPSS + KEV 우선순위 | EPSS 정렬 + KEV 표면화 + SLA 뱃지 + risk score | 보유 | +| CI/CD 연동 자동점검·빌드 차단 | GitHub App·웹훅·정책 게이트 | 보유(차단 게이트는 우위) | +| AI 모델 반입·AI 위협 분석 | ML-BOM ingest, G7 적합성, AI 라이선스 플래그 | 대부분 보유 | +| 라이선스·취약점 콘텐츠 한글화 | UI만 EN/KO, 라이선스 전문·CVE 상세는 영어 | **후보 1** | +| 정책 반영 맞춤 SBOM 출력 | 수출+정책+VEX 있으나 정책 필터 SBOM 프로파일 미명시 | **후보 3** | +| 다언어 도달성 분석(노이즈 70~95%↓) | Go(govulncheck)만 구현 | **후보 4 (BomLens 협업)** | +| 오픈소스 저장소 운영(내부 프록시) | 없음 | 역할경계(제외) | +| 서버 운영단계 상시 점검(호스트 에이전트) | 소스+컨테이너 스캔, 자동 재매칭 | 역할경계(제외) | + +## 후보 표 + +상태: open(격차 확인, 미착수) / in-progress / closed / 보류 + +| # | 후보 | 우선순위 | 상태 | 근거·범위 | +|---|---|---|---|---| +| C1 | 라이선스·취약점 콘텐츠 한글화 | P1 | open | 아래 상세 | +| C3 | 정책 반영 맞춤 SBOM 프로파일 | P3 | open | 아래 상세 | +| C4 | 다언어 도달성 분석 확대 (BomLens 협업) | P2 | open | 아래 상세 | + +번호는 대화에서 쓴 후보 번호(1·3·4)를 유지한다. 후보 2(오픈소스 반입 프론트도어)는 사용자 결정으로 제외했다. + +--- + +## C1 — 라이선스·취약점 콘텐츠 한글화 + +핵심: 지금은 화면 문자열만 EN/KO(react-i18next)이고, 정작 라이선스 전문(`apps/backend/services/license_texts/*.txt`)은 영어 SPDX 원문, CVE 상세 설명은 Trivy/NVD 영어 원문이다. UI i18n과 콘텐츠 번역은 별개 문제다. + +상용 근거: 포세라 기능 고도화 보도가 "라이선스 전문, 취약점 상세 정보 등을 한글로 제공"을 신기능으로 명시했다. 주 고객이 국내 금융권(신한금융그룹·현대캐피탈·우리은행)이며, 콘텐츠 한글화가 이들의 실제 요구다. TRUSCA는 한국어 GA 동시 출시를 표방하므로 시장 차별화 지점이다. + +범위 제안(단계적): +- (a) 라이선스 요약·의무사항 KO 번역 — 카탈로그 52종은 유한집합이라 사람 번역이 현실적이다. 의무사항 카탈로그(`obligation_catalog.py`)와 분류 판정 문구부터 시작한다. +- (b) CVE 상세 설명 KO — 대량·동적이라 기계번역 또는 지연 번역이 필요하다. 우선순위 높은 CVE(KEV·Critical)부터 선별 적용을 검토한다. + +주의: 라이선스 전문 자체는 법적 정본이 영어다. 번역본은 참고용임을 명시하고 원문을 병기한다(정본=영어 원칙 유지). NOTICE 번들에 들어가는 전문은 영어 유지. + +## C3 — 정책 반영 맞춤 SBOM 프로파일 + +핵심: SBOM 수출 4포맷(`sbom_export.py`), 라이선스 정책 엔진(`license_policy_service.py`), VEX 임베드가 이미 있다. 빠진 것은 "조직 정책을 반영·주석한 맞춤 SBOM 프로파일" 출력이다. + +상용 근거: 포세라가 "블랙덕 엔진에서 선택한 정책에 따라 조직의 보안 정책과 오픈소스 사용 기준을 반영한 맞춤형 SBOM 출력" 기능을 추가했다. Black Duck 2026.1도 SBOM visibility·정책 연동을 강조한다. + +범위 제안: 기존 수출·정책 자산을 재사용하는 작은 확장이다. 수출 시 정책 프로파일을 선택하면 위반 컴포넌트를 CycloneDX `properties`/SPDX annotation으로 주석하거나, 프로파일 규칙에 따라 필터한다(예: 금지 라이선스 강조, 정책 위반 표식, 조직 프로파일별 출력). 저비용·차별화 항목이라 C1 다음의 실용 후보다. + +## C4 — 다언어 도달성 분석 확대 (BomLens 협업) + +핵심: 현재 도달성 분석은 Go(`integrations/govulncheck.py`, `tasks/scan_reachability.py`)만 지원한다. Black Duck의 reachability는 다언어(Java·JavaScript 등)로 실제 호출되는 코드에 한정해 findings를 70~95% 줄인다 — 이것이 상용 대비 TRUSCA의 최대 격차다. + +협업 근거: 도달성 산출(언어별 call-graph 구성)은 단발 스캔 도구의 강점 영역이라 BomLens(`sktelecom/sbom-tools`) 역할에 가깝고, TRUSCA는 그 결과를 ingest해 findings 필터·우선순위로 표면화하는 소비자에 가깝다. 따라서 이 후보는 TRUSCA 단독이 아니라 BomLens와 공동 설계로 진행한다. + +역할 분담(초안): +- BomLens: 언어별 도달성/call-graph 산출을 스캔 산출물에 포함(단발 스캔 강점 확장). +- TRUSCA: 산출물을 ingest해 취약점 우선순위·필터에 반영하고, VEX(도달 불가 → not_affected)와 연결. + +선행 작업: BomLens 방향 문서(`bomlens-internal/identity-and-direction.md`)와 예고 기능을 확인하고, 공용 산출 포맷·역할 경계를 BomLens 팀과 먼저 협의한다. 고비용 전략 과제이므로 스코프 합의 전에는 착수하지 않는다. 이 항목은 [`bomlens-parity-review.md`](./bomlens-parity-review.md)의 "예고 기능 감시 목록"과 연동해 추적한다. + +--- + +## 리뷰 이력 + +| 일자 | 요약 | +|---|---| +| 2026-07-09 | 최초 작성. 보안뉴스 ISEC 2026 미리보기(포세라 위드 블랙덕) 계기로 상용 대조. 헤드라인 기능 다수는 이미 보유 확인, 후보 3건(C1 한글화·C3 맞춤 SBOM·C4 다언어 도달성) 등록. 후보 2(반입 프론트도어)·역할경계 2건(저장소 운영·서버 상시 점검) 제외 | From 5448e6701719dda156c81d088c3fb2d53214a8d3 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 02:00:05 +0900 Subject: [PATCH 08/10] fix(security): harden the K/L/M parsers per security review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Producer-Reviewer findings (Medium 3 / Low 3 / Info 2), all addressed: - CocoaPods ReDoS (M): the PODS-block regexes backtrack quadratically on a crafted line (measured 16s at 80k chars). Guards, in order: 2 MiB file cap before reading, 4 KiB per-line cap before the regex, and the work bound now counts EXAMINED lines (a never-matching block cannot bypass MAX_PODS). Regression test pins the pathological line under 1s. - eol_cycle VARCHAR(32) overflow (M): a 33+-digit crafted version segment used to overflow the column at flush time — OUTSIDE the per-component best-effort guard — aborting the whole persist. derive_cycle now treats over-long numeric segments (>15 chars) as underivable -> 'unknown'. - eol_sync_state.snapshot bloat (M): per-field 256-char bound in the feed compactor (drop, never truncate — a truncated version would match the wrong cycle) plus a 256 KiB size gate on the assembled dataset before the JSONB UPSERT (skip as 'snapshot_too_large', last-good survives). - EOL feed redirects (L): max_redirects=3 with the trust-extension rationale documented (endoflife.date fronts a redirecting CDN, so redirects can't be off entirely). - Scope-filter audit trail (L): the purls of dropped components are now recorded (bounded sample of 200 + exact totals) in ScopeFilterResult and scan_metadata.scope_filter.dropped_refs — counts alone are not reviewable when the npm predicate trusts an attacker-controlled lockfile classification. - Symlink guards (L): Podfile.lock and the Podfile-detection glob reject symlinks (cosign-module precedent). - LIKE metacharacter escape + assert->explicit guard (Info). --- apps/backend/integrations/cdxgen.py | 12 ++++- .../integrations/cocoapods_lockfile.py | 42 ++++++++++++++--- apps/backend/integrations/eol_feed.py | 30 +++++++++++- .../backend/integrations/sbom_scope_filter.py | 32 +++++++++++-- apps/backend/services/eol/eol_catalog.py | 12 +++++ apps/backend/tasks/eol_catalog_refresh.py | 46 ++++++++++++++++++- apps/backend/tasks/scan_source.py | 5 ++ .../test_cdxgen_cocoapods_exclude.py | 10 ++++ .../integrations/test_cocoapods_lockfile.py | 45 ++++++++++++++++++ .../tests/unit/integrations/test_eol_feed.py | 17 +++++++ .../integrations/test_sbom_scope_filter.py | 29 ++++++++++++ .../tests/unit/services/test_eol_catalog.py | 18 ++++++++ .../unit/tasks/test_eol_catalog_refresh.py | 26 +++++++++++ 13 files changed, 308 insertions(+), 16 deletions(-) diff --git a/apps/backend/integrations/cdxgen.py b/apps/backend/integrations/cdxgen.py index d0791ad6..c07d78ba 100644 --- a/apps/backend/integrations/cdxgen.py +++ b/apps/backend/integrations/cdxgen.py @@ -105,12 +105,20 @@ def _podfile_present(source_dir: Path) -> bool: surface. ``Pods/`` copies are ignored — ``Pods/Manifest.lock`` trees ship their own Podfiles that do not make the project a CocoaPods root. """ - if (source_dir / "Podfile").is_file(): + root_podfile = source_dir / "Podfile" + if root_podfile.is_file() and not root_podfile.is_symlink(): return True for pattern in ("*/Podfile", "*/*/Podfile"): for candidate in source_dir.glob(pattern): relative_parts = candidate.relative_to(source_dir).parts - if candidate.is_file() and "Pods" not in relative_parts: + if ( + candidate.is_file() + # Symlink guard — a symlinked Podfile (or one under a + # symlinked dir the glob followed) must not steer the flag + # off tree-local evidence (security-reviewer L3). + and not candidate.is_symlink() + and "Pods" not in relative_parts + ): return True return False diff --git a/apps/backend/integrations/cocoapods_lockfile.py b/apps/backend/integrations/cocoapods_lockfile.py index 48469043..0bc5b24c 100644 --- a/apps/backend/integrations/cocoapods_lockfile.py +++ b/apps/backend/integrations/cocoapods_lockfile.py @@ -53,6 +53,18 @@ # a hostile file could declare millions of synthetic lines. MAX_PODS = 20_000 +# File-size ceiling before ANY parsing. Real Podfile.locks are tens of KB; +# 2 MiB (the npm-reader intent) flags a hostile/corrupt file without reading +# it into memory (security-reviewer M1 — the regex cost below is per-line, +# so the byte cap must run first). +_MAX_LOCKFILE_BYTES = 2 * 1024 * 1024 + +# Per-line length ceiling. The PODS-block regexes carry a lazy `.+?` whose +# backtracking is quadratic on a pathological line (measured: 16s at 80k +# chars) — no real lockfile line approaches 4 KiB, so longer lines are +# skipped before the regex ever sees them (security-reviewer M1 ReDoS). +_MAX_LINE_CHARS = 4096 + # " - Alamofire (5.8.1)" / " - Moya (15.0.0):" (trailing colon when children # follow). Version group optional — a malformed entry without one is skipped. _POD_RE = re.compile(r"^ - (?P.+?)(?: \((?P[^)]*)\))?:?\s*$") @@ -135,16 +147,29 @@ def read_podfile_lock(source_dir: Path) -> CocoapodsLockfileData | None: info line flags it for future extension if real repos demand it. """ lock_path = source_dir / "Podfile.lock" + # Symlink guard (security-reviewer L3): a repo shipping + # ``Podfile.lock -> /etc/passwd`` must not make the worker read outside + # the source tree (the cosign module models the same defense). + if lock_path.is_symlink(): + log.warning("cocoapods_lockfile_symlink_rejected", path=str(lock_path)) + return None if not lock_path.is_file(): for pattern in ("*/Podfile.lock", "*/*/Podfile.lock"): for nested in source_dir.glob(pattern): - if "Pods" not in nested.parts: + if "Pods" not in nested.parts and not nested.is_symlink(): log.info( "cocoapods_lockfile_not_at_root", found=str(nested) ) break return None try: + if lock_path.stat().st_size > _MAX_LOCKFILE_BYTES: + log.warning( + "cocoapods_lockfile_too_large", + path=str(lock_path), + limit_bytes=_MAX_LOCKFILE_BYTES, + ) + return None lines = lock_path.read_text( encoding="utf-8", errors="replace" ).splitlines() @@ -160,7 +185,7 @@ def read_podfile_lock(source_dir: Path) -> CocoapodsLockfileData | None: edges: dict[str, set[str]] = {} in_pods = False current: str | None = None - parsed = 0 + examined = 0 for line in lines: if not in_pods: if line.rstrip() == "PODS:": @@ -170,12 +195,17 @@ def read_podfile_lock(source_dir: Path) -> CocoapodsLockfileData | None: # non-empty line such as ``DEPENDENCIES:``). if line and not line.startswith(" "): break + # Work bound counts EXAMINED lines, not matches — a hostile block of + # never-matching lines must not bypass the cap (security-reviewer M1). + examined += 1 + if examined > MAX_PODS: + log.warning("cocoapods_lockfile_pod_cap_exceeded", limit=MAX_PODS) + break + # ReDoS guard: skip pathological lines before the regexes see them. + if len(line) > _MAX_LINE_CHARS: + continue pod_match = _POD_RE.match(line) if pod_match: - parsed += 1 - if parsed > MAX_PODS: - log.warning("cocoapods_lockfile_pod_cap_exceeded", limit=MAX_PODS) - break name = pod_match.group("name").strip() version = (pod_match.group("ver") or "").strip() if not name or not version: diff --git a/apps/backend/integrations/eol_feed.py b/apps/backend/integrations/eol_feed.py index 207028f5..9c46b2f8 100644 --- a/apps/backend/integrations/eol_feed.py +++ b/apps/backend/integrations/eol_feed.py @@ -59,6 +59,14 @@ # Ceiling on cycles accepted per product — a real product has dozens. _MAX_CYCLES_PER_PRODUCT = 500 +# Ceiling on a single kept field VALUE. Real values are version strings and +# ISO dates (< 40 chars); without this a hostile mirror could pack a +# multi-hundred-KB string into ``latest`` on every cycle and bloat the +# persisted eol_sync_state.snapshot JSONB far past its few-KB contract +# (security-reviewer M3). Longer values are dropped, not truncated — a +# truncated version string would silently match the wrong cycle. +_MAX_FIELD_CHARS = 256 + class EolFeedUnavailable(Exception): """No product could be fetched at all (network dead, mirror broken). @@ -101,7 +109,20 @@ def _compact_cycles(payload: Any) -> list[dict[str, Any]] | None: for entry in payload[:_MAX_CYCLES_PER_PRODUCT]: if not isinstance(entry, dict): continue - compact.append({k: entry[k] for k in _KEEP_FIELDS if k in entry}) + kept: dict[str, Any] = {} + for key in _KEEP_FIELDS: + if key not in entry: + continue + value = entry[key] + # Field-value bound (M3): only scalar shapes the evaluator can + # read survive, and oversized strings are dropped outright. + if isinstance(value, str): + if len(value) <= _MAX_FIELD_CHARS: + kept[key] = value + elif isinstance(value, bool | int | float) or value is None: + kept[key] = value + if kept: + compact.append(kept) return compact if compact else None @@ -165,9 +186,16 @@ def fetch_eol_dataset( template = eol_feed_url_template() safe_host = _safe_host(template.replace("{product}", "probe")) owned = http is None + # follow_redirects with a TIGHT cap (security-reviewer L1): the env-only + # trust model covers the CONFIGURED host, and a redirect extends that + # trust to wherever the mirror points — endoflife.date fronts with a + # redirecting CDN so redirects can't be off entirely, but 3 hops bounds + # how far a compromised mirror can bounce the worker, and every fetch + # logs the (template) host so a bounce is attributable. client = http or httpx.Client( timeout=eol_refresh_timeout_seconds(), follow_redirects=True, + max_redirects=3, ) dataset: dict[str, Any] = { "_snapshot": datetime.now(tz=UTC).date().isoformat() diff --git a/apps/backend/integrations/sbom_scope_filter.py b/apps/backend/integrations/sbom_scope_filter.py index 67e22e39..0a7ebc73 100644 --- a/apps/backend/integrations/sbom_scope_filter.py +++ b/apps/backend/integrations/sbom_scope_filter.py @@ -71,6 +71,14 @@ # ``bomlens:*`` provenance convention). FILTER_PROPERTY_NAME = "trusca:scope_filter" +# Audit-trail bound (security-reviewer L2): the purls of dropped components +# are recorded (a bounded sample + the exact totals) so a reviewer can verify +# that nothing shipping was hidden behind an opaque count — the npm predicate +# trusts an attacker-controlled lockfile classification, so counts alone are +# not an auditable trail. 200 covers every realistic drop set; a hostile SBOM +# cannot bloat scan_metadata past it. +MAX_DROPPED_REFS_RECORDED = 200 + @dataclass(frozen=True) class ScopeFilterResult: @@ -90,6 +98,9 @@ class ScopeFilterResult: applied: bool dropped: dict[str, int] = field(default_factory=dict) kept_components: int = 0 + # Purls of the dropped components — the audit trail (bounded at + # MAX_DROPPED_REFS_RECORDED; ``dropped`` carries the exact totals). + dropped_refs: list[str] = field(default_factory=list) @property def total_dropped(self) -> int: @@ -173,6 +184,13 @@ def _filter( kept: list[Any] = [] dropped: dict[str, int] = {} + dropped_refs: list[str] = [] + + def _record_drop(ecosystem: str, purl: str) -> None: + dropped[ecosystem] = dropped.get(ecosystem, 0) + 1 + if len(dropped_refs) < MAX_DROPPED_REFS_RECORDED: + dropped_refs.append(purl) + for component in components: if not isinstance(component, dict): kept.append(component) # defensive: never drop what we can't read @@ -181,12 +199,13 @@ def _filter( purl = purl if isinstance(purl, str) else "" if maven_active and purl.startswith("pkg:maven/"): if component.get("scope") in _MAVEN_DROP_SCOPES: - dropped["maven"] = dropped.get("maven", 0) + 1 + _record_drop("maven", purl) continue - if node_active and purl.startswith("pkg:npm/"): - assert npm_lock is not None # implied by node_active + # ``node_active`` implies a parsed lockfile; the explicit None check + # (not an assert — stripped under ``python -O``) keeps that visible. + if node_active and npm_lock is not None and purl.startswith("pkg:npm/"): if npm_lock.scope_for_purl(purl) == "dev": - dropped["npm"] = dropped.get("npm", 0) + 1 + _record_drop("npm", purl) continue kept.append(component) @@ -198,7 +217,10 @@ def _filter( _prune_dependencies(sbom, _kept_refs(sbom, kept)) _stamp_filter_property(sbom, dropped) return ScopeFilterResult( - applied=True, dropped=dropped, kept_components=len(kept) + applied=True, + dropped=dropped, + kept_components=len(kept), + dropped_refs=dropped_refs, ) diff --git a/apps/backend/services/eol/eol_catalog.py b/apps/backend/services/eol/eol_catalog.py index 795f196e..008d0dea 100644 --- a/apps/backend/services/eol/eol_catalog.py +++ b/apps/backend/services/eol/eol_catalog.py @@ -50,6 +50,14 @@ _LEADING_INT = re.compile(r"^([0-9]+)") +# Ceiling on a single numeric segment in a derived cycle. The persisted +# column is VARCHAR(32) (0038) and no real release cycle approaches double +# digits per segment; a crafted 33+-digit "version" in a hostile SBOM would +# otherwise overflow the column at flush time — OUTSIDE the per-component +# best-effort guard — and abort the whole persist (security-reviewer M2). +# Over-long segments make the cycle underivable → "unknown", never a guess. +_MAX_CYCLE_SEGMENT_CHARS = 15 + EolState = Literal["eol", "supported", "unknown"] # Closed vocabulary persisted into component_versions.eol_state (0038). @@ -171,6 +179,10 @@ def derive_cycle(version: str, granularity: str) -> str | None: match = _LEADING_INT.match(part) if not match: break + if len(match.group(1)) > _MAX_CYCLE_SEGMENT_CHARS: + # Pathological numeric run — no real cycle looks like this, and + # persisting it would overflow VARCHAR(32). Underivable. + return None segments.append(match.group(1)) if not segments: return None diff --git a/apps/backend/tasks/eol_catalog_refresh.py b/apps/backend/tasks/eol_catalog_refresh.py index 2cf2d41b..3c83fc3e 100644 --- a/apps/backend/tasks/eol_catalog_refresh.py +++ b/apps/backend/tasks/eol_catalog_refresh.py @@ -31,6 +31,7 @@ from __future__ import annotations +import json import time from datetime import UTC, date, datetime from typing import Any @@ -52,6 +53,14 @@ log = structlog.get_logger("tasks.eol_catalog_refresh") +# Ceiling on the assembled snapshot's serialized size before it may be +# persisted into the eol_sync_state JSONB row (security-reviewer M3). The +# real dataset is ~16 KB; 256 KiB (the shared JSONB row-size posture) is +# far above any legitimate sweep and far below what a hostile mirror could +# otherwise pack in. Over the ceiling the tick is treated like a bad feed: +# skip, keep the last-good snapshot, retry next week. +_MAX_SNAPSHOT_BYTES = 256 * 1024 + def _snapshot_to_dataset(raw: Any) -> EolDataset | None: """Parse a stored sync-state snapshot dict into an EolDataset (or None).""" @@ -203,6 +212,21 @@ def _fetch_half(summary: dict[str, Any]) -> EolDataset | None: ) return None + snapshot_bytes = len(json.dumps(result.dataset, separators=(",", ":"))) + if snapshot_bytes > _MAX_SNAPSHOT_BYTES: + # Size gate before the JSONB persist (M3) — the per-field bound in + # eol_feed already makes this unreachable for a real feed, so + # tripping it means a hostile/broken mirror. Same posture as a bad + # sweep: last-good snapshot survives. + summary["skipped"] = True + summary["skipped_reason"] = "snapshot_too_large" + log.warning( + "eol_catalog_refresh_snapshot_too_large", + bytes=snapshot_bytes, + limit_bytes=_MAX_SNAPSHOT_BYTES, + ) + return None + summary["products_ok"] = len(result.fetched) summary["products_failed"] = len(result.failed) summary["snapshot"] = result.dataset @@ -289,15 +313,33 @@ def refresh_eol_catalog(self: Any) -> dict[str, Any]: # set) OR already stamped (the clear set — covers a shrunk map). # %40-encoded npm scopes are matched by a second LIKE per rule # whose prefix contains '@' (cdxgen emits both spellings). + # LIKE-metacharacter escape (security-reviewer INFO): the "%40" + # spelling would otherwise read as a one-char wildcard ("%4" + + # "0")… harmless today because evaluate() re-checks the literal + # prefix, but escape properly so a future map entry containing + # '%'/'_' in a real coordinate cannot silently over-match. + def _escaped_prefix(prefix: str) -> str: + return ( + prefix.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + ) + like_clauses = [] for rule in rules: like_clauses.append( - ComponentVersion.purl_with_version.like(rule.purl_prefix + "%") + ComponentVersion.purl_with_version.like( + _escaped_prefix(rule.purl_prefix) + "%", escape="\\" + ) ) if "@" in rule.purl_prefix: like_clauses.append( ComponentVersion.purl_with_version.like( - rule.purl_prefix.replace("@", "%40") + "%" + _escaped_prefix( + rule.purl_prefix.replace("@", "%40") + ) + + "%", + escape="\\", ) ) like_clauses.append(ComponentVersion.eol_state.is_not(None)) diff --git a/apps/backend/tasks/scan_source.py b/apps/backend/tasks/scan_source.py index cbad42f4..73d66e0b 100644 --- a/apps/backend/tasks/scan_source.py +++ b/apps/backend/tasks/scan_source.py @@ -1280,6 +1280,11 @@ def _record_scope_filter( "applied": True, "dropped": dict(result.dropped), "kept": result.kept_components, + # Audit trail (security-reviewer L2): the identities of + # what was removed, bounded at the module cap — counts + # alone are not reviewable when the npm predicate trusts + # an attacker-controlled lockfile classification. + "dropped_refs": list(result.dropped_refs), } scan.scan_metadata = merged session.commit() diff --git a/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py b/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py index 3ae1f948..d2f9d8d8 100644 --- a/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py +++ b/apps/backend/tests/unit/integrations/test_cdxgen_cocoapods_exclude.py @@ -113,3 +113,13 @@ def test_no_exclude_type_without_podfile( source_dir=source, output_dir=tmp_path / "out", backend="real" ) assert "--exclude-type" not in captured_cmd[-1] + + +def test_symlinked_podfile_not_detected(tmp_path: Path) -> None: + # L3 — a symlinked Podfile must not steer the --exclude-type flag. + outside = tmp_path / "outside-podfile" + outside.write_text("platform :ios\n", encoding="utf-8") + source = tmp_path / "src" + source.mkdir() + (source / "Podfile").symlink_to(outside) + assert cdxgen._podfile_present(source) is False diff --git a/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py b/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py index 76ed5c68..9f2cf604 100644 --- a/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py +++ b/apps/backend/tests/unit/integrations/test_cocoapods_lockfile.py @@ -233,3 +233,48 @@ def test_merge_tolerates_missing_arrays() -> None: def test_merge_never_raises_on_hostile_document() -> None: assert merge_into_sbom({"components": "not-a-list"}, _data()) == 0 + + +# --------------------------------------------------------------------------- +# Security-review hardening (M1 ReDoS / L3 symlinks) +# --------------------------------------------------------------------------- + + +def test_pathological_long_line_is_skipped_fast(tmp_path: Path) -> None: + # ReDoS guard: an 80k-char crafted line used to take ~16s in the POD + # regex; the per-line cap must skip it before the regex runs. The real + # pod on the next line still parses. + evil = " - a" + " " * 80_000 + "a" + src = _write_lock(tmp_path, f"PODS:\n{evil}\n - Real (1.0.0)\n") + import time + + started = time.monotonic() + data = read_podfile_lock(src) + elapsed = time.monotonic() - started + assert elapsed < 1.0 # the unguarded regex alone took >15s here + assert data is not None + assert set(data.pods) == {"Real"} + + +def test_oversized_lockfile_rejected_before_parsing(tmp_path: Path) -> None: + filler = "# junk\n" * 400_000 # ~2.8 MiB — over _MAX_LOCKFILE_BYTES + src = _write_lock(tmp_path, "PODS:\n - Real (1.0.0)\n" + filler) + assert read_podfile_lock(src) is None + + +def test_symlinked_lockfile_rejected(tmp_path: Path) -> None: + outside = tmp_path / "outside.txt" + outside.write_text("PODS:\n - Sneaky (1.0.0)\n", encoding="utf-8") + src = tmp_path / "repo" + src.mkdir() + (src / "Podfile.lock").symlink_to(outside) + assert read_podfile_lock(src) is None + + +def test_examined_line_cap_bounds_never_matching_blocks(tmp_path: Path) -> None: + # The work bound counts EXAMINED lines: a hostile block of non-matching + # indented lines cannot buy unbounded regex passes. + lines = ["PODS:"] + [" x" for _ in range(MAX_PODS + 100)] + [" - Tail (1.0)"] + src = _write_lock(tmp_path, "\n".join(lines) + "\n") + data = read_podfile_lock(src) + assert data is None or "Tail" not in (data.pods if data else {}) diff --git a/apps/backend/tests/unit/integrations/test_eol_feed.py b/apps/backend/tests/unit/integrations/test_eol_feed.py index eb97c6e6..9537bb84 100644 --- a/apps/backend/tests/unit/integrations/test_eol_feed.py +++ b/apps/backend/tests/unit/integrations/test_eol_feed.py @@ -134,3 +134,20 @@ def handler(request: httpx.Request) -> httpx.Response: fetch_eol_dataset(["express"], http=_client(handler)) assert seen == ["https://mirror.internal/eol/express.json"] + + +def test_oversized_field_values_are_dropped_not_persisted() -> None: + # security-reviewer M3 — a hostile mirror packing a huge string into a + # kept field must not bloat the persisted snapshot; the entry survives + # with the oversized field removed (drop, never truncate — a truncated + # version string would match the wrong cycle). + huge = "x" * 100_000 + cycles = [{"cycle": "4", "eol": False, "latest": huge}] + + def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, json=cycles) + + result = fetch_eol_dataset(["express"], http=_client(handler)) + entry = result.dataset["express"][0] + assert entry["cycle"] == "4" + assert "latest" not in entry diff --git a/apps/backend/tests/unit/integrations/test_sbom_scope_filter.py b/apps/backend/tests/unit/integrations/test_sbom_scope_filter.py index d9f9e0d1..8075af8b 100644 --- a/apps/backend/tests/unit/integrations/test_sbom_scope_filter.py +++ b/apps/backend/tests/unit/integrations/test_sbom_scope_filter.py @@ -328,3 +328,32 @@ def test_rewrite_sbom_file_unserializable_payload_keeps_original( target.write_text('{"old": true}', encoding="utf-8") assert rewrite_sbom_file(target, {"bad": object()}) is False assert json.loads(target.read_text(encoding="utf-8")) == {"old": True} + + +# --------------------------------------------------------------------------- +# Audit trail — dropped identities (security-reviewer L2) +# --------------------------------------------------------------------------- + + +def test_dropped_refs_record_the_removed_purls() -> None: + lock = _lock({"pkg:npm/jest@29.7.0": "dev"}) + sbom = _sbom( + [_maven("keep", "required"), _maven("junit", "optional"), _npm("jest", "29.7.0")] + ) + result = filter_sbom_to_runtime_scope(sbom, npm_lock=lock) + assert set(result.dropped_refs) == { + "pkg:maven/com.example/junit@1.0.0", + "pkg:npm/jest@29.7.0", + } + + +def test_dropped_refs_bounded_while_counts_stay_exact() -> None: + from integrations.sbom_scope_filter import MAX_DROPPED_REFS_RECORDED + + total = MAX_DROPPED_REFS_RECORDED + 50 + components = [_maven("keep", "required")] + [ + _maven(f"test-dep-{i}", "optional") for i in range(total) + ] + result = filter_sbom_to_runtime_scope(_sbom(components), npm_lock=None) + assert result.dropped == {"maven": total} # exact totals survive the cap + assert len(result.dropped_refs) == MAX_DROPPED_REFS_RECORDED diff --git a/apps/backend/tests/unit/services/test_eol_catalog.py b/apps/backend/tests/unit/services/test_eol_catalog.py index 5aec768a..d498eff2 100644 --- a/apps/backend/tests/unit/services/test_eol_catalog.py +++ b/apps/backend/tests/unit/services/test_eol_catalog.py @@ -313,3 +313,21 @@ def test_stamp_none_verdict_leaves_row_untouched() -> None: ) assert row.eol_state is None assert row.eol_evaluated_at is None + + +def test_pathological_numeric_version_is_underivable() -> None: + # security-reviewer M2 — a 33+-digit "major" would overflow the + # VARCHAR(32) eol_cycle column at flush time, OUTSIDE the per-component + # best-effort guard. Over-long segments must read as underivable. + assert derive_cycle("1" * 40, "major") is None + assert derive_cycle("1" * 40 + ".2", "major.minor") is None + verdict = evaluate( + "pkg:npm/express@" + "1" * 40, + "1" * 40, + rules=_rules(), + dataset=_dataset(), + today=TODAY, + ) + assert verdict is not None + assert verdict.state == "unknown" + assert verdict.cycle is None diff --git a/apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py b/apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py index e7d31a2d..2986c0fd 100644 --- a/apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py +++ b/apps/backend/tests/unit/tasks/test_eol_catalog_refresh.py @@ -240,3 +240,29 @@ def test_clear_stamp_nulls_everything_once() -> None: stamp_time = row.eol_evaluated_at assert _clear_stamp(row) is False # type: ignore[arg-type] assert row.eol_evaluated_at == stamp_time # idempotent — not re-dirtied + + +def test_fetch_half_rejects_an_oversized_snapshot( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # security-reviewer M3 — the assembled dataset is size-gated before the + # JSONB persist; an over-limit sweep skips like a bad feed. + from integrations.eol_feed import EolFetchResult + + monkeypatch.setenv("EOL_REFRESH_ENABLED", "true") + + def _bloated(products: list[str]) -> EolFetchResult: + dataset: dict[str, Any] = {"_snapshot": "2026-07-12"} + for product in products: + # ~60 KB per product × 10 products > the 256 KiB ceiling while + # every individual field stays under the eol_feed field bound. + dataset[product] = [ + {"cycle": str(i), "eol": False, "latest": "v" * 250} + for i in range(250) + ] + return EolFetchResult(dataset=dataset, fetched=list(products), failed=[]) + + monkeypatch.setattr("tasks.eol_catalog_refresh.fetch_eol_dataset", _bloated) + summary = _base_summary(snapshot=None) + assert _fetch_half(summary) is None + assert summary["skipped_reason"] == "snapshot_too_large" From f81a02a06d57006220a7c1116bad342fe314da3d Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 02:06:47 +0900 Subject: [PATCH 09/10] chore(security): apply the two non-blocking re-review residuals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cap each recorded dropped ref at 512 chars so 200 hostile mega-purls cannot inflate the scan_metadata JSONB row. - Correct the _podfile_present comment: the symlink guard covers the leaf only (glob traverses directory symlinks) — acceptable because the flag decides a CLI argument, not a read path; the read guard lives in read_podfile_lock. --- apps/backend/integrations/cdxgen.py | 8 +++++--- apps/backend/integrations/sbom_scope_filter.py | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/backend/integrations/cdxgen.py b/apps/backend/integrations/cdxgen.py index c07d78ba..8ca3cf32 100644 --- a/apps/backend/integrations/cdxgen.py +++ b/apps/backend/integrations/cdxgen.py @@ -113,9 +113,11 @@ def _podfile_present(source_dir: Path) -> bool: relative_parts = candidate.relative_to(source_dir).parts if ( candidate.is_file() - # Symlink guard — a symlinked Podfile (or one under a - # symlinked dir the glob followed) must not steer the flag - # off tree-local evidence (security-reviewer L3). + # Symlink guard on the LEAF only — glob still traverses + # directory symlinks, which is acceptable: the flag decides + # a CLI argument, not a read path, and a repo author could + # equally commit a real Podfile. The out-of-tree READ guard + # lives in cocoapods_lockfile.read_podfile_lock. and not candidate.is_symlink() and "Pods" not in relative_parts ): diff --git a/apps/backend/integrations/sbom_scope_filter.py b/apps/backend/integrations/sbom_scope_filter.py index 0a7ebc73..185104e7 100644 --- a/apps/backend/integrations/sbom_scope_filter.py +++ b/apps/backend/integrations/sbom_scope_filter.py @@ -189,7 +189,10 @@ def _filter( def _record_drop(ecosystem: str, purl: str) -> None: dropped[ecosystem] = dropped.get(ecosystem, 0) + 1 if len(dropped_refs) < MAX_DROPPED_REFS_RECORDED: - dropped_refs.append(purl) + # Per-ref char cap: 200 hostile mega-purls must not inflate the + # scan_metadata JSONB row (re-review residual). Real purls are + # far under 512; a clipped ref still identifies the component. + dropped_refs.append(purl[:512]) for component in components: if not isinstance(component, dict): From e358f630c85fb3ac26eb0efd077ad89be2c63148 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sat, 11 Jul 2026 07:40:30 +0900 Subject: [PATCH 10/10] fix(scan,api): findings from full-stack verification of Phases K/L/M MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-stack verification (docker-compose dev + Playwright + real worker scans of public repos) surfaced three defects the unit suites structurally could not see: - Hand-built responses dropped the new fields (third instance of this class): the component drawer built ComponentDetailResponse without the eol_* block and the overview built ProjectOverviewResponse without eol_count — Pydantic silently defaulted both, so the DB and the list carried the verdict while the drawer/overview read null (caught by components_eol e2e E3/E4). Fixed, and a new AST completeness contract (test_handbuilt_response_completeness.py) requires a keyword for every model field in both constructions so the fourth instance fails in CI. - Root-keyed consumers missed the git-clone subdirectory: _fetch_source returns workspace/source while the git path clones into source/repo, so the prep lockfile generators, the scope filter's and CocoaPods fill-in's lockfile reads, persist's W4-D npm scope enrichment and language detection all silently no-opped on every git scan (zip extractions, which land at source/ itself, were fine — which is why the fixture e2e never saw it). _resolve_project_root now hands them the repo root. Verified live: a chalk.git scan now drops 648 lockfile-dev components (47 kept) with the audit refs recorded; before the fix the same scan persisted all 695 with no telemetry. - components_scope_filter.spec.ts seeded with the default component prefix, which collides with earlier runs' catalog purls (component-mode purls are not run-suffixed) — the seed failed on every run after the first. Now mints a per-run prefix like the sibling EOL spec. Also verified live: maven-simple (all test-scope, no required) is left whole by the hasScopes guard exactly as designed, with Usage=Optional visible; the iOS guard survives a real cdxgen 12.3.3 run against a Podfile tree in the worker and fills 3 pods + the subspec edge back in. --- apps/backend/api/v1/components.py | 8 +++ apps/backend/api/v1/projects.py | 4 ++ apps/backend/tasks/scan_source.py | 38 ++++++++-- .../test_handbuilt_response_completeness.py | 69 +++++++++++++++++++ .../tasks/test_scan_source_scope_filter.py | 22 ++++++ .../tests/e2e/components_scope_filter.spec.ts | 5 ++ 6 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 apps/backend/tests/unit/api/test_handbuilt_response_completeness.py diff --git a/apps/backend/api/v1/components.py b/apps/backend/api/v1/components.py index 2662f2a0..039c05bc 100644 --- a/apps/backend/api/v1/components.py +++ b/apps/backend/api/v1/components.py @@ -80,6 +80,14 @@ async def get_component_detail_endpoint( # this since W2 #31 but the hand-built response omitted it, so the # wire payload was always ``null``. dependency_scope=payload["dependency_scope"], + # Phase M — same hand-built-response drop class as the line above + # (caught by the components_eol e2e: DB and list carried the verdict, + # the drawer read null). + eol_state=payload["eol_state"], + eol_product=payload["eol_product"], + eol_cycle=payload["eol_cycle"], + eol_date=payload["eol_date"], + eol_source=payload["eol_source"], created_at=payload["created_at"], updated_at=payload["updated_at"], ) diff --git a/apps/backend/api/v1/projects.py b/apps/backend/api/v1/projects.py index 9f7ab4aa..0d21db22 100644 --- a/apps/backend/api/v1/projects.py +++ b/apps/backend/api/v1/projects.py @@ -450,6 +450,10 @@ async def get_project_overview_endpoint( project_id=payload["project_id"], project_name=payload["project_name"], total_components=payload["total_components"], + # Phase M — hand-built-response completeness (see + # tests/unit/api/test_handbuilt_response_completeness.py: this + # construction has silently dropped fields before). + eol_count=payload["eol_count"], severity_distribution=payload["severity_distribution"], license_distribution=payload["license_distribution"], risk_score=payload["risk_score"], diff --git a/apps/backend/tasks/scan_source.py b/apps/backend/tasks/scan_source.py index 73d66e0b..ff24d6a8 100644 --- a/apps/backend/tasks/scan_source.py +++ b/apps/backend/tasks/scan_source.py @@ -352,13 +352,24 @@ def _run_pipeline( mock_only=False, ) + # The PROJECT root vs the workspace parent: the git path clones into + # ``source/repo`` while the zip path extracts into ``source/`` itself, and + # ``_fetch_source`` returns the PARENT either way. cdxgen is fine with the + # parent (``-r`` recurses), but every ROOT-KEYED consumer — the prep + # lockfile generators, the scope filter's / CocoaPods fill-in's lockfile + # reads, persist's npm scope enrichment, language detection — must see the + # actual project root or it silently no-ops on every git scan (found by + # the Phase K real-scan verification; the W4-D npm enrichment had the same + # latent gap). + project_root = _resolve_project_root(source_dir) + # Detect the source environment (BomLens-style language → cdxgen image map) # and record it on the scan. Increment 2 only observes: the in-process # executor still handles every environment. SCAN_EXECUTOR=local_docker / # k8s_job (later increments) consume ``detected_env`` to route an # environment-specific cdxgen sidecar. Best-effort — a detection error must # never fail the scan, so we fall back to "unknown" and continue. - detected_env = _detect_and_record_env(scan_uuid, source_dir) + detected_env = _detect_and_record_env(scan_uuid, project_root) # Stage 2.5 + 3 — build-prep + cdxgen, behind the ScanExecutor abstraction. # cdxgen needs a populated lockfile to enumerate transitive deps for Ruby / @@ -388,7 +399,7 @@ def _run_pipeline( ) gen_result = executor.generate_sbom( gen_request, - prep=lambda: _prepare_for_cdxgen(source_dir=source_dir, scan_uuid=scan_uuid), + prep=lambda: _prepare_for_cdxgen(source_dir=project_root, scan_uuid=scan_uuid), stage=lambda stage: _set_stage(scan_uuid, stage), # P2 #8c — stream cdxgen stdout/stderr lines onto the scan WebSocket # so the drawer can render a live tool trace. Best-effort: a publish @@ -408,7 +419,7 @@ def _run_pipeline( # trusca:scope_filter property describe the FINAL document (cocoapods # purls pass both keep-predicates untouched). Best-effort, never fatal. _merge_cocoapods_components( - scan_uuid=scan_uuid, cdxgen_result=cdxgen_result, source_dir=source_dir + scan_uuid=scan_uuid, cdxgen_result=cdxgen_result, source_dir=project_root ) # Stage 3.25 — runtime-scope post-filter (Phase K). MUST run before the @@ -417,7 +428,7 @@ def _run_pipeline( # artifact, vulnerability matching) sees ONE consistent filtered document. # Best-effort: any failure leaves the SBOM exactly as cdxgen wrote it. _apply_scope_filter( - scan_uuid=scan_uuid, cdxgen_result=cdxgen_result, source_dir=source_dir + scan_uuid=scan_uuid, cdxgen_result=cdxgen_result, source_dir=project_root ) _persist_artifact( @@ -520,7 +531,9 @@ def _run_pipeline( session, scan_uuid=scan_uuid, sbom=cdxgen_result.sbom, - source_dir=source_dir, + # The PROJECT root (not the workspace parent) so the npm-lockfile + # scope enrichment actually finds package-lock.json on git scans. + source_dir=project_root, ) if scancode_detections: try: @@ -1214,6 +1227,21 @@ def _merge_cocoapods_components( ) +def _resolve_project_root(source_dir: Path) -> Path: + """The scanned PROJECT's root inside the fetch output. + + ``_fetch_source`` returns ``workspace/source`` while the git path clones + into ``workspace/source/repo`` (the zip path extracts into ``source/`` + itself). Root-keyed consumers — the prep lockfile generators, the scope + filter's and CocoaPods fill-in's lockfile reads, persist's npm scope + enrichment, language detection — need the repo root; handing them the + parent made every one of them silently no-op on git scans. cdxgen is + unaffected either way (``-r`` recurses from the parent). + """ + repo = source_dir / "repo" + return repo if repo.is_dir() else source_dir + + def _apply_scope_filter( *, scan_uuid: uuid.UUID, diff --git a/apps/backend/tests/unit/api/test_handbuilt_response_completeness.py b/apps/backend/tests/unit/api/test_handbuilt_response_completeness.py new file mode 100644 index 00000000..85494a94 --- /dev/null +++ b/apps/backend/tests/unit/api/test_handbuilt_response_completeness.py @@ -0,0 +1,69 @@ +"""Contract — hand-built response constructions pass EVERY model field. + +Several endpoints construct their response model field by field (not +``**payload`` / ``model_validate``). That pattern has silently dropped +fields THREE times now: ``dependency_scope`` (W2 #31 — the wire carried +``null`` for two releases), the Phase M ``eol_*`` block on the component +drawer, and ``eol_count`` on the project overview (masked by the schema's +``default=0``, caught by the components_eol e2e). A dropped field never +fails a type check — Pydantic happily defaults it — so only a completeness +contract catches the next one. + +Each assertion walks the endpoint module's AST and requires a keyword +argument for every field of the hand-built model. Extend ``_cases`` whenever +a new endpoint hand-builds its response. +""" + +from __future__ import annotations + +import ast +from pathlib import Path +from typing import Any + +import pytest + + +def _passed_kwargs(module: Any, model_name: str) -> set[str]: + source = Path(module.__file__).read_text(encoding="utf-8") + for node in ast.walk(ast.parse(source)): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == model_name + ): + return {kw.arg for kw in node.keywords if kw.arg is not None} + return set() + + +def _cases() -> list[tuple[str, Any, type[Any]]]: + import api.v1.components as components_module + import api.v1.projects as projects_module + from schemas.project_detail import ( + ComponentDetailResponse, + ProjectOverviewResponse, + ) + + return [ + ("components", components_module, ComponentDetailResponse), + ("projects-overview", projects_module, ProjectOverviewResponse), + ] + + +@pytest.mark.parametrize( + ("label", "module", "model"), + _cases(), + ids=lambda value: value if isinstance(value, str) else "", +) +def test_every_response_field_is_passed_by_the_endpoint( + label: str, module: Any, model: type[Any] +) -> None: + passed = _passed_kwargs(module, model.__name__) + assert passed, f"{model.__name__} construction not found in {label}" + + expected = set(model.model_fields) + missing = expected - passed + assert not missing, ( + f"{label} builds {model.__name__} without {sorted(missing)} — the " + f"field silently defaults on the wire (the dependency_scope / eol_* " + f"/ eol_count drop class)" + ) diff --git a/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py index c77c1018..7936dbd0 100644 --- a/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py +++ b/apps/backend/tests/unit/tasks/test_scan_source_scope_filter.py @@ -358,3 +358,25 @@ def test_ingest_pipeline_never_references_the_scope_filter() -> None: } | {node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute)} assert "_apply_scope_filter" not in names assert "filter_sbom_to_runtime_scope" not in names + + +# --------------------------------------------------------------------------- +# _resolve_project_root — the git-clone repo/ subdirectory (real-scan finding) +# --------------------------------------------------------------------------- + + +def test_project_root_resolves_the_git_clone_subdir(tmp_path: Path) -> None: + # git path: _fetch_source clones into source/repo — root-keyed consumers + # (lockfile reads, prep, detection) must see repo/, not the parent. + from tasks.scan_source import _resolve_project_root + + (tmp_path / "repo").mkdir() + assert _resolve_project_root(tmp_path) == tmp_path / "repo" + + +def test_project_root_falls_back_for_zip_extractions(tmp_path: Path) -> None: + # zip path: extraction lands at source/ itself — no repo/ subdir. + from tasks.scan_source import _resolve_project_root + + (tmp_path / "package.json").write_text("{}", encoding="utf-8") + assert _resolve_project_root(tmp_path) == tmp_path diff --git a/apps/frontend/tests/e2e/components_scope_filter.spec.ts b/apps/frontend/tests/e2e/components_scope_filter.spec.ts index 5bbd7ea1..927dd99f 100644 --- a/apps/frontend/tests/e2e/components_scope_filter.spec.ts +++ b/apps/frontend/tests/e2e/components_scope_filter.spec.ts @@ -35,6 +35,10 @@ import { seedE2eUser, type SeedSummary } from "../_harness/seed"; const FILTERED_PROJECT = "ci-scope-filter"; const EXPECTED_DROPPED = 15; // seed writes {maven: 3, npm: 12} +// Component-mode purls are NOT run-suffixed by the seed — a repeated default +// prefix collides with earlier runs' catalog rows (see +// vulnerabilities_epss.spec.ts), so mint one per run. +const PREFIX = `scopeseed${Date.now().toString(36)}`; let sharedPage: Page; let seedFailed = false; @@ -48,6 +52,7 @@ test.describe.serial("@components scope-filter summary note", () => { projectNames: [FILTERED_PROJECT], withScan: true, componentCount: 5, + componentPrefix: PREFIX, }); } catch { seedFailed = true;