@opensip-cli/tool-osv-scanner 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @fileoverview OSV-Scanner JSON → normalized `Signal[]` (ADR-0090 §4, brief §4).
3
+ *
4
+ * OSV-Scanner emits a nested document: `results[]` (one per scanned source, e.g. a
5
+ * lockfile) → `packages[]` (one per resolved dependency) → `vulnerabilities[]`
6
+ * (one per advisory) plus a sibling `groups[]` (advisories collapsed by alias,
7
+ * carrying `max_severity` — a CVSS base-score string). One normalized
8
+ * `security` {@link Signal} is emitted per vulnerability.
9
+ *
10
+ * Severity resolution (richest first):
11
+ * 1. `groups[].max_severity` — a CVSS base score string ("7.5") → {@link
12
+ * cvssToSeverity} (FIRST/NVD v3 bands).
13
+ * 2. `database_specific.severity` — the GHSA LABEL (`CRITICAL|HIGH|MODERATE|LOW`;
14
+ * **MODERATE ⇒ medium**, the only non-obvious mapping).
15
+ * 3. neither present ⇒ default `medium` (a known vulnerability with an unknown
16
+ * score is still worth surfacing; documented).
17
+ * The raw label is preserved on `metadata.nativeSeverity`; the raw CVSS string on
18
+ * `metadata.cvss`.
19
+ *
20
+ * OSV carries no per-line anchor (the finding is the lockfile-pinned dependency),
21
+ * so `code.line`/`code.column` are omitted — only `code.file` (the source path) is
22
+ * set. No credentials are present, so there is nothing to redact.
23
+ *
24
+ * Pure: defensive JSON navigation (never throws on malformed input → `[]`).
25
+ */
26
+ import type { Signal } from '@opensip-cli/core';
27
+ import type { AdapterRunContext, ParsedScannerOutput } from '@opensip-cli/external-tool-adapter';
28
+ /**
29
+ * Parse OSV-Scanner JSON output into normalized signals. An empty/clean run
30
+ * (`{"results":[]}`, or "nothing scanned") yields no findings; each vulnerability
31
+ * maps to a `security` signal whose severity comes from the CVSS `max_severity`
32
+ * (preferred) or the GHSA label (`MODERATE ⇒ medium`), defaulting to `medium`.
33
+ */
34
+ export declare function parseOsvJson(raw: ParsedScannerOutput, _ctx: AdapterRunContext): readonly Signal[];
35
+ //# sourceMappingURL=parse-osv-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-osv-json.d.ts","sourceRoot":"","sources":["../src/parse-osv-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAaH,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AA+IjG;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,iBAAiB,GAAG,SAAS,MAAM,EAAE,CAYjG"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @fileoverview OSV-Scanner JSON → normalized `Signal[]` (ADR-0090 §4, brief §4).
3
+ *
4
+ * OSV-Scanner emits a nested document: `results[]` (one per scanned source, e.g. a
5
+ * lockfile) → `packages[]` (one per resolved dependency) → `vulnerabilities[]`
6
+ * (one per advisory) plus a sibling `groups[]` (advisories collapsed by alias,
7
+ * carrying `max_severity` — a CVSS base-score string). One normalized
8
+ * `security` {@link Signal} is emitted per vulnerability.
9
+ *
10
+ * Severity resolution (richest first):
11
+ * 1. `groups[].max_severity` — a CVSS base score string ("7.5") → {@link
12
+ * cvssToSeverity} (FIRST/NVD v3 bands).
13
+ * 2. `database_specific.severity` — the GHSA LABEL (`CRITICAL|HIGH|MODERATE|LOW`;
14
+ * **MODERATE ⇒ medium**, the only non-obvious mapping).
15
+ * 3. neither present ⇒ default `medium` (a known vulnerability with an unknown
16
+ * score is still worth surfacing; documented).
17
+ * The raw label is preserved on `metadata.nativeSeverity`; the raw CVSS string on
18
+ * `metadata.cvss`.
19
+ *
20
+ * OSV carries no per-line anchor (the finding is the lockfile-pinned dependency),
21
+ * so `code.line`/`code.column` are omitted — only `code.file` (the source path) is
22
+ * set. No credentials are present, so there is nothing to redact.
23
+ *
24
+ * Pure: defensive JSON navigation (never throws on malformed input → `[]`).
25
+ */
26
+ import { createSignal } from '@opensip-cli/core';
27
+ import { asArray, asObject, cvssToSeverity, getString, parseCvss, safeParseJson, withNativeSeverity, } from '@opensip-cli/external-tool-adapter';
28
+ /** Read the parsed OSV document from the descriptor payload, defensively. */
29
+ function osvDocument(raw) {
30
+ // The run loop pre-parses JSON for `kind: 'json'`; fall back to the raw bytes
31
+ // (the acceptance-harness path) so the parser is total either way.
32
+ if (raw.json !== undefined)
33
+ return asObject(raw.json);
34
+ const parsed = safeParseJson(raw.raw);
35
+ return parsed.ok ? asObject(parsed.value) : undefined;
36
+ }
37
+ /**
38
+ * Map a GHSA `database_specific.severity` LABEL to a four-bucket severity.
39
+ * `MODERATE` (GHSA's term) and `MEDIUM` both collapse to `medium`; an unknown
40
+ * label yields `undefined` so the caller can fall through to the default.
41
+ */
42
+ function labelToSeverity(label) {
43
+ switch (label?.toUpperCase()) {
44
+ case 'CRITICAL': {
45
+ return 'critical';
46
+ }
47
+ case 'HIGH': {
48
+ return 'high';
49
+ }
50
+ case 'MODERATE':
51
+ case 'MEDIUM': {
52
+ return 'medium';
53
+ }
54
+ case 'LOW': {
55
+ return 'low';
56
+ }
57
+ default: {
58
+ return undefined;
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * The `max_severity` CVSS string for a vulnerability: the `groups[]` entry whose
64
+ * `ids` include this vuln id carries it (OSV collapses aliased advisories into one
65
+ * group). Empty/absent ⇒ `undefined`.
66
+ */
67
+ function maxSeverityForVuln(groups, vulnId) {
68
+ for (const group of groups) {
69
+ const ids = asArray(asObject(group)?.ids) ?? [];
70
+ if (ids.includes(vulnId)) {
71
+ const score = getString(group, 'max_severity');
72
+ if (score !== undefined && score.length > 0)
73
+ return score;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ /** Compose the human message: `<summary> (<pkg>@<version>)`, with defensive fallbacks. */
79
+ function buildMessage(summary, ruleId, pkg, installed) {
80
+ const base = summary ?? ruleId;
81
+ if (pkg === undefined)
82
+ return base;
83
+ const ref = installed === undefined ? pkg : `${pkg}@${installed}`;
84
+ return `${base} (${ref})`;
85
+ }
86
+ /**
87
+ * Normalize one OSV vulnerability (within a package + source) to a {@link Signal}.
88
+ * Returns `undefined` for a non-object entry so a malformed element is skipped
89
+ * rather than throwing.
90
+ */
91
+ function normalizeVulnerability(entry, groups, context) {
92
+ const vuln = asObject(entry);
93
+ if (vuln === undefined)
94
+ return undefined;
95
+ const ruleId = getString(vuln, 'id') ?? 'osv-vulnerability';
96
+ const summary = getString(vuln, 'summary');
97
+ const details = getString(vuln, 'details');
98
+ const aliases = (asArray(vuln.aliases) ?? []).filter((a) => typeof a === 'string');
99
+ const label = getString(asObject(vuln.database_specific), 'severity');
100
+ // Severity: CVSS max_severity (numeric) first, then the GHSA label, then default.
101
+ const cvss = maxSeverityForVuln(groups, ruleId);
102
+ const cvssScore = parseCvss(cvss);
103
+ const severity = cvssScore === undefined ? (labelToSeverity(label) ?? 'medium') : cvssToSeverity(cvssScore);
104
+ const metadata = withNativeSeverity({
105
+ aliases,
106
+ ecosystem: context.ecosystem ?? null,
107
+ pkg: context.pkg ?? null,
108
+ installed: context.installed ?? null,
109
+ cvss: cvss ?? null,
110
+ },
111
+ // Preserve the scanner's own severity label (null when it emits none).
112
+ label ?? null);
113
+ return createSignal({
114
+ source: 'osv-scanner',
115
+ category: 'security',
116
+ severity,
117
+ ruleId,
118
+ message: buildMessage(summary, ruleId, context.pkg, context.installed),
119
+ ...(details === undefined ? {} : { suggestion: details }),
120
+ // OSV anchors a finding at the lockfile (no line) — set only `code.file`.
121
+ code: { file: context.sourcePath },
122
+ metadata,
123
+ });
124
+ }
125
+ /** Normalize every vulnerability inside one `packages[]` entry. */
126
+ function normalizePackage(entry, sourcePath) {
127
+ const packageEntry = asObject(entry);
128
+ if (packageEntry === undefined)
129
+ return [];
130
+ const pkgInfo = asObject(packageEntry.package);
131
+ const context = {
132
+ sourcePath,
133
+ pkg: getString(pkgInfo, 'name'),
134
+ installed: getString(pkgInfo, 'version'),
135
+ ecosystem: getString(pkgInfo, 'ecosystem'),
136
+ };
137
+ const groups = asArray(packageEntry.groups) ?? [];
138
+ const signals = [];
139
+ for (const vuln of asArray(packageEntry.vulnerabilities) ?? []) {
140
+ const signal = normalizeVulnerability(vuln, groups, context);
141
+ if (signal !== undefined)
142
+ signals.push(signal);
143
+ }
144
+ return signals;
145
+ }
146
+ /**
147
+ * Parse OSV-Scanner JSON output into normalized signals. An empty/clean run
148
+ * (`{"results":[]}`, or "nothing scanned") yields no findings; each vulnerability
149
+ * maps to a `security` signal whose severity comes from the CVSS `max_severity`
150
+ * (preferred) or the GHSA label (`MODERATE ⇒ medium`), defaulting to `medium`.
151
+ */
152
+ export function parseOsvJson(raw, _ctx) {
153
+ const doc = osvDocument(raw);
154
+ const results = asArray(doc?.results) ?? [];
155
+ const signals = [];
156
+ for (const result of results) {
157
+ const sourcePath = getString(asObject(result)?.source, 'path') ?? '';
158
+ for (const packageEntry of asArray(asObject(result)?.packages) ?? []) {
159
+ signals.push(...normalizePackage(packageEntry, sourcePath));
160
+ }
161
+ }
162
+ return signals;
163
+ }
164
+ //# sourceMappingURL=parse-osv-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-osv-json.js","sourceRoot":"","sources":["../src/parse-osv-json.ts"],"names":[],"mappings":"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"}
package/dist/tool.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `@opensip-cli/tool-osv-scanner` Tool descriptor (ADR-0090 / ADR-0091 / ADR-0092).
3
+ *
4
+ * The second External Tool Adapter: it wraps the user-installed `osv-scanner`
5
+ * dependency-vulnerability scanner as an ordinary opensip-cli `Tool` via {@link
6
+ * defineExternalToolAdapter}. The substrate owns binary resolution, the run loop
7
+ * (resolve → execFile → ingest → normalize → persist via the host artifact seam),
8
+ * provenance, and the auto-added `doctor`/`version` commands; this module declares
9
+ * only the osv-scanner identity, the wrapped binary, and the `scan` command (args
10
+ * + JSON parser).
11
+ *
12
+ * Like gitleaks, this is a JSON adapter (it routes OSV-Scanner's `--format json`,
13
+ * which is richer than its SARIF output, through a per-adapter `parse`, NOT the
14
+ * shared `ingestSarif`).
15
+ *
16
+ * Layer 4: imports the substrate + `@opensip-cli/core` ONLY — never the CLI,
17
+ * output, or any other adapter (dependency-cruiser enforced).
18
+ *
19
+ * This is an OPT-IN, installed tool (NOT in `bundled-tools.manifest.json`): the
20
+ * host never imports this runtime; an `opensip osv-scanner` / `opensip osv`
21
+ * invocation forks a worker that re-discovers + imports it and runs the handler.
22
+ * Installed tools are deny-by-default — a run needs
23
+ * `OPENSIP_CLI_ALLOW_INSTALLED_TOOLS` to include the osv-scanner id.
24
+ */
25
+ import type { Tool, ToolIdentity } from '@opensip-cli/core';
26
+ import type { AdapterRunContext } from '@opensip-cli/external-tool-adapter';
27
+ /** Human/aliased identity (`opensip osv-scanner` / `opensip osv`). */
28
+ export declare const OSV_SCANNER_IDENTITY: ToolIdentity;
29
+ /** Stable UUID identity (ADR-0048); mirrors `opensipTools.stableId` in package.json. */
30
+ export declare const OSV_SCANNER_STABLE_ID = "d25a4471-3289-4660-b5ab-63830072d0e1";
31
+ /**
32
+ * Normalize the `osv-scanner --version` stdout to a bare semver. OSV-Scanner
33
+ * prints a multi-line banner whose first line is e.g. `osv-scanner version: 1.9.1`
34
+ * (or, on some builds, just `1.9.1`); take the first semver-shaped token and strip
35
+ * a leading `v`.
36
+ *
37
+ * VERIFY-against-installed-binary: exact `osv-scanner --version` output format.
38
+ */
39
+ export declare function parseOsvVersion(stdout: string): string;
40
+ /**
41
+ * Build the osv-scanner scan argv (no shell — args are passed to `execFile`).
42
+ * Scans the project recursively (`-r <root>`) and writes a JSON report to the
43
+ * host-owned artifact path the substrate composes for this run. OSV-Scanner ships
44
+ * an embedded/offline advisory DB, so the scan is local-only (no network, no auth).
45
+ *
46
+ * VERIFY-against-installed-binary: this is the stable v1.x flat invocation
47
+ * (`osv-scanner --format json --output <path> -r <root>`). OSV-Scanner v2.x
48
+ * reorganized the surface to `osv-scanner scan source -r <root>`; if v2 becomes the
49
+ * floor, switch on the doctor-detected version. (The flags `--format`, `--output`,
50
+ * `-r` are stable across both.)
51
+ */
52
+ export declare function buildScanArgs(ctx: AdapterRunContext): readonly string[];
53
+ /**
54
+ * The osv-scanner external-tool adapter `Tool`. The host loads it by name through
55
+ * the installed-tool worker-dispatch path (the barrel re-exports it as `tool`).
56
+ */
57
+ export declare const tool: Tool;
58
+ //# sourceMappingURL=tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAOH,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAE5E,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,EAAE,YAGlC,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,qBAAqB,yCAAyC,CAAC;AAE5E;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,iBAAiB,GAAG,SAAS,MAAM,EAAE,CAEvE;AAED;;;GAGG;AACH,eAAO,MAAM,IAAI,EAAE,IAgDjB,CAAC"}
package/dist/tool.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `@opensip-cli/tool-osv-scanner` Tool descriptor (ADR-0090 / ADR-0091 / ADR-0092).
3
+ *
4
+ * The second External Tool Adapter: it wraps the user-installed `osv-scanner`
5
+ * dependency-vulnerability scanner as an ordinary opensip-cli `Tool` via {@link
6
+ * defineExternalToolAdapter}. The substrate owns binary resolution, the run loop
7
+ * (resolve → execFile → ingest → normalize → persist via the host artifact seam),
8
+ * provenance, and the auto-added `doctor`/`version` commands; this module declares
9
+ * only the osv-scanner identity, the wrapped binary, and the `scan` command (args
10
+ * + JSON parser).
11
+ *
12
+ * Like gitleaks, this is a JSON adapter (it routes OSV-Scanner's `--format json`,
13
+ * which is richer than its SARIF output, through a per-adapter `parse`, NOT the
14
+ * shared `ingestSarif`).
15
+ *
16
+ * Layer 4: imports the substrate + `@opensip-cli/core` ONLY — never the CLI,
17
+ * output, or any other adapter (dependency-cruiser enforced).
18
+ *
19
+ * This is an OPT-IN, installed tool (NOT in `bundled-tools.manifest.json`): the
20
+ * host never imports this runtime; an `opensip osv-scanner` / `opensip osv`
21
+ * invocation forks a worker that re-discovers + imports it and runs the handler.
22
+ * Installed tools are deny-by-default — a run needs
23
+ * `OPENSIP_CLI_ALLOW_INSTALLED_TOOLS` to include the osv-scanner id.
24
+ */
25
+ import { readPackageVersion } from '@opensip-cli/core';
26
+ import { defineExternalToolAdapter } from '@opensip-cli/external-tool-adapter';
27
+ import { parseOsvJson } from './parse-osv-json.js';
28
+ /** Human/aliased identity (`opensip osv-scanner` / `opensip osv`). */
29
+ export const OSV_SCANNER_IDENTITY = {
30
+ name: 'osv-scanner',
31
+ aliases: ['osv'],
32
+ };
33
+ /** Stable UUID identity (ADR-0048); mirrors `opensipTools.stableId` in package.json. */
34
+ export const OSV_SCANNER_STABLE_ID = 'd25a4471-3289-4660-b5ab-63830072d0e1';
35
+ /**
36
+ * Normalize the `osv-scanner --version` stdout to a bare semver. OSV-Scanner
37
+ * prints a multi-line banner whose first line is e.g. `osv-scanner version: 1.9.1`
38
+ * (or, on some builds, just `1.9.1`); take the first semver-shaped token and strip
39
+ * a leading `v`.
40
+ *
41
+ * VERIFY-against-installed-binary: exact `osv-scanner --version` output format.
42
+ */
43
+ export function parseOsvVersion(stdout) {
44
+ // Fully bounded ({1,5} digit runs, {1,2} dotted segments) so the matcher is
45
+ // linear — major.minor[.patch], optional leading `v`.
46
+ const match = /v?(\d{1,5}(?:\.\d{1,5}){1,2})/.exec(stdout);
47
+ return match?.[1] ?? stdout.trim();
48
+ }
49
+ /**
50
+ * Build the osv-scanner scan argv (no shell — args are passed to `execFile`).
51
+ * Scans the project recursively (`-r <root>`) and writes a JSON report to the
52
+ * host-owned artifact path the substrate composes for this run. OSV-Scanner ships
53
+ * an embedded/offline advisory DB, so the scan is local-only (no network, no auth).
54
+ *
55
+ * VERIFY-against-installed-binary: this is the stable v1.x flat invocation
56
+ * (`osv-scanner --format json --output <path> -r <root>`). OSV-Scanner v2.x
57
+ * reorganized the surface to `osv-scanner scan source -r <root>`; if v2 becomes the
58
+ * floor, switch on the doctor-detected version. (The flags `--format`, `--output`,
59
+ * `-r` are stable across both.)
60
+ */
61
+ export function buildScanArgs(ctx) {
62
+ return ['--format', 'json', '--output', ctx.artifactPath('osv.json'), '-r', ctx.projectRoot];
63
+ }
64
+ /**
65
+ * The osv-scanner external-tool adapter `Tool`. The host loads it by name through
66
+ * the installed-tool worker-dispatch path (the barrel re-exports it as `tool`).
67
+ */
68
+ export const tool = defineExternalToolAdapter({
69
+ identity: OSV_SCANNER_IDENTITY,
70
+ metadata: {
71
+ id: OSV_SCANNER_STABLE_ID,
72
+ version: readPackageVersion(import.meta.url),
73
+ description: 'Dependency vulnerability scanning via OSV-Scanner',
74
+ adapterPackage: '@opensip-cli/tool-osv-scanner',
75
+ },
76
+ binary: {
77
+ command: 'osv-scanner',
78
+ versionArgs: ['--version'],
79
+ versionParse: parseOsvVersion,
80
+ // The flat `--format json --output -r` invocation is stable from v1.4.
81
+ // VERIFY-against-installed-binary (v2.x moved verbs under `scan source`).
82
+ minVersion: '1.4.0',
83
+ // Operator pin (config `binaries.osv-scanner.path` / `OPENSIP_OSV_SCANNER_BIN`)
84
+ // beats PATH; resolution never fetches a binary.
85
+ resolution: ['config', 'path'],
86
+ installHint: 'Install osv-scanner: https://google.github.io/osv-scanner/installation/ (brew install osv-scanner)',
87
+ },
88
+ // OSV-Scanner queries its embedded/offline advisory DB via execFile — no
89
+ // network, no credentials.
90
+ network: 'local-only',
91
+ commands: [
92
+ {
93
+ name: 'scan',
94
+ description: 'Scan project dependencies for known vulnerabilities (OSV-Scanner)',
95
+ args: buildScanArgs,
96
+ output: { kind: 'json', path: 'osv.json' },
97
+ // ADR-0091 Phase-0 decision 4 (OSV): `0` clean, `1` findings, `>=2` fault.
98
+ // The exception is `128` ("no packages/lockfiles found") — a genuinely CLEAN
99
+ // no-op (a project with no dependency manifests), NOT a fault, so it joins the
100
+ // `ok` set. With `errorFrom: 2`, `127` (general/usage error) still faults.
101
+ // VERIFY-against-installed-binary: the exact nothing-scanned code (recollection
102
+ // 128) across versions.
103
+ exitCodes: { ok: [0, 128], findings: [1], errorFrom: 2 },
104
+ parse: parseOsvJson,
105
+ // A3 (no `excludeScan`): OSV-Scanner only parses recognized lockfiles/SBOMs,
106
+ // never an arbitrary JSON report, so it does NOT re-detect opensip's own
107
+ // persisted reports under `.runtime/` — there is no fingerprint churn to guard
108
+ // against, and OSV-Scanner v1.x exposes no path-exclude flag (only `--no-ignore`
109
+ // for .gitignore and vuln-id ignores). It is therefore left without an exclusion.
110
+ },
111
+ ],
112
+ // Scanner output is line-volatile → the line-shift-tolerant message hash, not the
113
+ // host `ruleId|file|line|col` default. Stamped worker-side in the run loop.
114
+ fingerprintStrategy: 'message-hash',
115
+ });
116
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.js","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAE/E,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAKnD,sEAAsE;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAiB;IAChD,IAAI,EAAE,aAAa;IACnB,OAAO,EAAE,CAAC,KAAK,CAAC;CACjB,CAAC;AAEF,wFAAwF;AACxF,MAAM,CAAC,MAAM,qBAAqB,GAAG,sCAAsC,CAAC;AAE5E;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,KAAK,GAAG,+BAA+B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3D,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,GAAsB;IAClD,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;AAC/F,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAS,yBAAyB,CAAC;IAClD,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE;QACR,EAAE,EAAE,qBAAqB;QACzB,OAAO,EAAE,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;QAC5C,WAAW,EAAE,mDAAmD;QAChE,cAAc,EAAE,+BAA+B;KAChD;IACD,MAAM,EAAE;QACN,OAAO,EAAE,aAAa;QACtB,WAAW,EAAE,CAAC,WAAW,CAAC;QAC1B,YAAY,EAAE,eAAe;QAC7B,uEAAuE;QACvE,0EAA0E;QAC1E,UAAU,EAAE,OAAO;QACnB,gFAAgF;QAChF,iDAAiD;QACjD,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC9B,WAAW,EACT,oGAAoG;KACvG;IACD,yEAAyE;IACzE,2BAA2B;IAC3B,OAAO,EAAE,YAAY;IACrB,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,MAAM;YACZ,WAAW,EAAE,mEAAmE;YAChF,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE;YAC1C,2EAA2E;YAC3E,6EAA6E;YAC7E,+EAA+E;YAC/E,2EAA2E;YAC3E,gFAAgF;YAChF,wBAAwB;YACxB,SAAS,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE;YACxD,KAAK,EAAE,YAAY;YACnB,6EAA6E;YAC7E,yEAAyE;YACzE,+EAA+E;YAC/E,iFAAiF;YACjF,kFAAkF;SACnF;KACF;IACD,kFAAkF;IAClF,4EAA4E;IAC5E,mBAAmB,EAAE,cAAc;CACpC,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,138 @@
1
+ {
2
+ "name": "@opensip-cli/tool-osv-scanner",
3
+ "version": "0.1.15",
4
+ "license": "Apache-2.0",
5
+ "description": "External Tool Adapter for OSV-Scanner — wraps the osv-scanner dependency vulnerability scanner as an opensip-cli tool (opensip osv-scanner / opensip osv)",
6
+ "keywords": [
7
+ "opensip-cli",
8
+ "static-analysis",
9
+ "code-quality"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/opensip-ai/opensip-cli.git",
14
+ "directory": "packages/tool-osv-scanner"
15
+ },
16
+ "homepage": "https://github.com/opensip-ai/opensip-cli",
17
+ "bugs": {
18
+ "url": "https://github.com/opensip-ai/opensip-cli/issues"
19
+ },
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": "./dist/index.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "LICENSE",
29
+ "NOTICE"
30
+ ],
31
+ "opensipTools": {
32
+ "kind": "tool",
33
+ "id": "osv-scanner",
34
+ "identity": {
35
+ "name": "osv-scanner",
36
+ "aliases": [
37
+ "osv"
38
+ ]
39
+ },
40
+ "stableId": "d25a4471-3289-4660-b5ab-63830072d0e1",
41
+ "apiVersion": 1,
42
+ "requires": [
43
+ {
44
+ "resource": "subprocess",
45
+ "reason": "Executes the user-installed osv-scanner binary via execFile (no shell)"
46
+ },
47
+ {
48
+ "resource": "filesystem",
49
+ "reason": "Reads the project working tree and writes the raw scan artifact under .runtime/artifacts"
50
+ }
51
+ ],
52
+ "commands": [
53
+ {
54
+ "name": "osv-scanner",
55
+ "description": "Scan project dependencies for known vulnerabilities (OSV-Scanner)",
56
+ "aliases": [
57
+ "osv"
58
+ ],
59
+ "commonFlags": [
60
+ "json",
61
+ "cwd",
62
+ "quiet",
63
+ "verbose",
64
+ "debug",
65
+ "reportTo",
66
+ "apiKey",
67
+ "open"
68
+ ],
69
+ "options": [
70
+ {
71
+ "flag": "--gate-save",
72
+ "description": "Architecture-gate: save current findings as baseline in the project SQLite store (mutually exclusive with --gate-compare)",
73
+ "default": false
74
+ },
75
+ {
76
+ "flag": "--gate-compare",
77
+ "description": "Architecture-gate: compare current findings against the saved baseline; exit 1 on regression",
78
+ "default": false
79
+ }
80
+ ],
81
+ "scope": "project",
82
+ "output": "raw-stream",
83
+ "rawStreamReason": "runtime-render-dispatch"
84
+ },
85
+ {
86
+ "name": "doctor",
87
+ "description": "Check that the osv-scanner binary is installed and ready",
88
+ "parent": "osv-scanner",
89
+ "commonFlags": [
90
+ "json",
91
+ "cwd"
92
+ ],
93
+ "scope": "none",
94
+ "output": "raw-stream",
95
+ "rawStreamReason": "diagnostic-gate"
96
+ },
97
+ {
98
+ "name": "version",
99
+ "description": "Print the resolved osv-scanner binary version",
100
+ "parent": "osv-scanner",
101
+ "commonFlags": [
102
+ "json",
103
+ "cwd"
104
+ ],
105
+ "scope": "none",
106
+ "output": "raw-stream",
107
+ "rawStreamReason": "diagnostic-gate"
108
+ }
109
+ ],
110
+ "config": {
111
+ "namespace": "osv-scanner",
112
+ "schema": {
113
+ "type": "object",
114
+ "properties": {
115
+ "binaries": {
116
+ "type": "object"
117
+ }
118
+ }
119
+ }
120
+ }
121
+ },
122
+ "dependencies": {
123
+ "typescript": "~6.0.3",
124
+ "@opensip-cli/external-tool-adapter": "0.1.15",
125
+ "@opensip-cli/contracts": "0.1.15",
126
+ "@opensip-cli/core": "0.1.15"
127
+ },
128
+ "devDependencies": {
129
+ "@types/node": "^24.13.2",
130
+ "vitest": "^4.1.8"
131
+ },
132
+ "scripts": {
133
+ "build": "tsc",
134
+ "test": "vitest run --passWithNoTests",
135
+ "typecheck": "tsc --noEmit",
136
+ "clean": "rm -rf dist"
137
+ }
138
+ }