pkgxray 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/bin/audit.js +240 -0
- package/bin/mcp-server.js +358 -0
- package/package.json +59 -0
- package/src/auditor.js +730 -0
- package/src/providers/anthropic.js +64 -0
- package/src/providers/gemini.js +66 -0
- package/src/providers/index.js +40 -0
- package/src/providers/openai.js +75 -0
- package/src/quarantine.js +519 -0
- package/src/reasoner.js +265 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jack Adams-Lovell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# pkgxray
|
|
2
|
+
|
|
3
|
+
Local CLI + MCP server for triaging whether an AI coding-agent extension, Codex
|
|
4
|
+
plugin, Claude Code extension, or npm package is safe to install — from supplied
|
|
5
|
+
evidence or by fetching a real npm tarball into a sandboxed quarantine.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g pkgxray
|
|
11
|
+
# or use one-shot via npx:
|
|
12
|
+
npx pkgxray guard npm:some-package@1.2.3
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It is intentionally conservative. It only reports evidence it can cite from
|
|
16
|
+
metadata or source text, and it returns one of:
|
|
17
|
+
|
|
18
|
+
- `safe`: no high- or medium-risk indicators in the provided evidence
|
|
19
|
+
- `review`: incomplete evidence or privileged capability needing manual review
|
|
20
|
+
- `block`: high-severity indicators such as prompt injection, credential access,
|
|
21
|
+
persistence, obfuscation plus execution, or likely exfiltration
|
|
22
|
+
|
|
23
|
+
## CLI
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pkgxray --file examples/evidence.json
|
|
27
|
+
pkgxray --format json --file examples/evidence.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Guard an extension before handing it to an agent:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pkgxray guard ./some-local-extension
|
|
34
|
+
pkgxray guard npm:some-mcp-server@1.2.3 --format json
|
|
35
|
+
pkgxray guard ./some-local-extension --promote-to ./approved/some-local-extension
|
|
36
|
+
pkgxray guard npm:is-number@7.0.0 --no-source-scan --format json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The guard flow stages the extension in a private quarantine directory, audits
|
|
40
|
+
that staged copy, and only promotes it when policy allows. It does not run
|
|
41
|
+
`npm install`, package lifecycle scripts, build steps, or extension code.
|
|
42
|
+
|
|
43
|
+
For npm references, guard order is:
|
|
44
|
+
|
|
45
|
+
1. Resolve package metadata from the npm registry.
|
|
46
|
+
2. Query OSV for the exact package/version.
|
|
47
|
+
3. If OSV reports vulnerabilities, block before tarball download.
|
|
48
|
+
4. If no vulnerabilities are reported, download and extract the tarball into
|
|
49
|
+
quarantine.
|
|
50
|
+
5. Collect source evidence and run the static audit unless `--no-source-scan`
|
|
51
|
+
is set.
|
|
52
|
+
|
|
53
|
+
The JSON output includes timing fields:
|
|
54
|
+
|
|
55
|
+
- `stageMs`: local copy or npm metadata resolution
|
|
56
|
+
- `vulnerabilityPrecheckMs`: OSV lookup time
|
|
57
|
+
- `downloadMs`: tarball download, hash, and extraction time
|
|
58
|
+
- `sourceCollectionMs`: capped source-file collection time
|
|
59
|
+
- `auditMs`: static audit time
|
|
60
|
+
|
|
61
|
+
Guard decisions:
|
|
62
|
+
|
|
63
|
+
- `allow`: safe verdict; promotion can happen
|
|
64
|
+
- `review`: do not promote by default; a human should inspect the quarantine
|
|
65
|
+
- `block`: high-severity evidence; do not install
|
|
66
|
+
|
|
67
|
+
By default, only `safe` promotes. Use `--policy allow-review` only when you want
|
|
68
|
+
review-grade packages copied into the destination for manual handling.
|
|
69
|
+
|
|
70
|
+
Input JSON:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"packageName": "example-extension",
|
|
75
|
+
"npmMetadata": {},
|
|
76
|
+
"githubMetadata": {},
|
|
77
|
+
"webPresence": {},
|
|
78
|
+
"sourceFiles": {
|
|
79
|
+
"package.json": "{\"name\":\"example-extension\"}",
|
|
80
|
+
"index.js": "module.exports = {}"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## MCP Server
|
|
86
|
+
|
|
87
|
+
Use the stdio server from any MCP-capable agent:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"pkgxray": {
|
|
93
|
+
"command": "pkgxray-mcp"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The server exposes three tools:
|
|
100
|
+
|
|
101
|
+
- `audit_agent_extension_supply_chain` — zero-dep static heuristics
|
|
102
|
+
- `guard_agent_extension_install` — stage, vuln-check, audit a real package
|
|
103
|
+
- `reason_about_extension_supply_chain` — Claude-powered authoritative verdict (requires `ANTHROPIC_API_KEY`)
|
|
104
|
+
|
|
105
|
+
Tool arguments:
|
|
106
|
+
|
|
107
|
+
- `packageName`: optional package or extension name
|
|
108
|
+
- `npmMetadata`: optional npm metadata object or text
|
|
109
|
+
- `githubMetadata`: optional GitHub metadata object or text
|
|
110
|
+
- `webPresence`: optional web presence object or text
|
|
111
|
+
- `sourceFiles`: required map of file path to source text, or array of file objects
|
|
112
|
+
- `outputFormat`: `markdown` or `json`
|
|
113
|
+
|
|
114
|
+
`guard_agent_extension_install` accepts `reference`, optional `quarantineRoot`,
|
|
115
|
+
optional `promoteTo`, `policy`, `force`, and `outputFormat`.
|
|
116
|
+
|
|
117
|
+
`reason_about_extension_supply_chain` accepts the same evidence shape as
|
|
118
|
+
`audit_agent_extension_supply_chain`, plus optional `model` (default
|
|
119
|
+
`claude-opus-4-7`) and `maxFiles` (default 200). It returns a JSON verdict per
|
|
120
|
+
the prompt's schema (`verdict`, `summary`, `findings`, `evidenceGaps`,
|
|
121
|
+
`promotable`) plus `usage` and `latencyMs`.
|
|
122
|
+
|
|
123
|
+
## Reasoning mode (`--reason`)
|
|
124
|
+
|
|
125
|
+
Layer an LLM-powered authoritative verdict on top of the static heuristics.
|
|
126
|
+
Supports Anthropic (Claude), OpenAI (GPT), and Google (Gemini).
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Anthropic (default)
|
|
130
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
131
|
+
npm install -g @anthropic-ai/sdk
|
|
132
|
+
pkgxray --reason --file evidence.json
|
|
133
|
+
pkgxray guard npm:some-pkg --reason --format json
|
|
134
|
+
|
|
135
|
+
# OpenAI
|
|
136
|
+
export OPENAI_API_KEY=sk-...
|
|
137
|
+
npm install -g openai
|
|
138
|
+
pkgxray guard npm:some-pkg --reason --reason-provider openai
|
|
139
|
+
|
|
140
|
+
# Gemini
|
|
141
|
+
export GEMINI_API_KEY=...
|
|
142
|
+
npm install -g @google/generative-ai
|
|
143
|
+
pkgxray guard npm:some-pkg --reason --reason-model gemini-2.5-pro
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Provider is selected by `--reason-provider <anthropic|openai|gemini>` or
|
|
147
|
+
auto-detected from the model prefix (`claude-*` → anthropic, `gpt-*`/`o*` →
|
|
148
|
+
openai, `gemini-*` → gemini). Defaults: `claude-opus-4-7`, `gpt-5`,
|
|
149
|
+
`gemini-2.5-pro` — overridable with `--reason-model`.
|
|
150
|
+
|
|
151
|
+
The Anthropic path uses adaptive thinking, `effort: "high"`, and caches the
|
|
152
|
+
system prompt for 5-minute TTL (~90% cheaper on prompt tokens for repeated
|
|
153
|
+
calls in the window). OpenAI uses strict structured outputs against the same
|
|
154
|
+
JSON Schema. Gemini uses JSON-mode responses.
|
|
155
|
+
|
|
156
|
+
Source files are capped at 200 files / 32 KB each / 500 KB total before
|
|
157
|
+
sending — override with `--reason-max-files`.
|
|
158
|
+
|
|
159
|
+
The reasoning verdict supersedes the static decision when `--reason` is used.
|
|
160
|
+
Exit codes: `0` = safe, `2` = block, `3` = review.
|
|
161
|
+
|
|
162
|
+
## Browser Extension
|
|
163
|
+
|
|
164
|
+
The `browser-extension/` folder is a Chrome-compatible Manifest V3 unpacked
|
|
165
|
+
extension. It runs entirely locally and requests no browser permissions.
|
|
166
|
+
|
|
167
|
+
Load the `browser-extension/` folder from a checkout of this repo as an
|
|
168
|
+
unpacked extension.
|
|
169
|
+
|
|
170
|
+
In Chrome:
|
|
171
|
+
|
|
172
|
+
1. Open `chrome://extensions`.
|
|
173
|
+
2. Enable Developer Mode.
|
|
174
|
+
3. Choose Load unpacked.
|
|
175
|
+
4. Select the `browser-extension` folder above.
|
|
176
|
+
|
|
177
|
+
In Dia, try the same flow if Dia exposes Chromium extension management. If Dia
|
|
178
|
+
does not currently allow unpacked extensions, use Chrome for testing and keep the
|
|
179
|
+
MCP/CLI version for agent workflows.
|
|
180
|
+
|
|
181
|
+
## Local Development
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npm run build:browser
|
|
185
|
+
npm test
|
|
186
|
+
npm run audit:evidence -- --file examples/evidence.json
|
|
187
|
+
```
|
package/bin/audit.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const { auditEvidence, renderMarkdown } = require("../src/auditor");
|
|
6
|
+
const { guardExtension } = require("../src/quarantine");
|
|
7
|
+
const { reasonAbout } = require("../src/reasoner");
|
|
8
|
+
|
|
9
|
+
function printUsage() {
|
|
10
|
+
process.stderr.write(
|
|
11
|
+
[
|
|
12
|
+
"Usage:",
|
|
13
|
+
" pkgxray < evidence.json",
|
|
14
|
+
" pkgxray --format json < evidence.json",
|
|
15
|
+
" pkgxray --file evidence.json --format markdown",
|
|
16
|
+
" pkgxray --reason --file evidence.json",
|
|
17
|
+
" pkgxray guard <npm-package|npm:name@version|./path> [--reason] [--promote-to dir] [--no-source-scan]",
|
|
18
|
+
"",
|
|
19
|
+
"Evidence JSON fields:",
|
|
20
|
+
" packageName, npmMetadata, githubMetadata, webPresence, sourceFiles",
|
|
21
|
+
"",
|
|
22
|
+
"--reason consults an LLM as an authoritative verdict on top of the static",
|
|
23
|
+
"heuristics. Provider auto-detected from --reason-model, or pass",
|
|
24
|
+
"--reason-provider <anthropic|openai|gemini>. Defaults: anthropic +",
|
|
25
|
+
"claude-opus-4-7. Each provider needs its own env key (ANTHROPIC_API_KEY,",
|
|
26
|
+
"OPENAI_API_KEY, GEMINI_API_KEY) and SDK installed.",
|
|
27
|
+
""
|
|
28
|
+
].join("\n")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const options = { command: "audit", format: "markdown", file: null };
|
|
34
|
+
if (argv[0] === "guard") {
|
|
35
|
+
options.command = "guard";
|
|
36
|
+
options.reference = argv[1];
|
|
37
|
+
argv = argv.slice(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
41
|
+
const arg = argv[i];
|
|
42
|
+
if (arg === "--help" || arg === "-h") {
|
|
43
|
+
options.help = true;
|
|
44
|
+
} else if (arg === "--format") {
|
|
45
|
+
options.format = argv[++i];
|
|
46
|
+
} else if (arg === "--file") {
|
|
47
|
+
options.file = argv[++i];
|
|
48
|
+
} else if (arg === "--quarantine-root") {
|
|
49
|
+
options.quarantineRoot = argv[++i];
|
|
50
|
+
} else if (arg === "--promote-to") {
|
|
51
|
+
options.promoteTo = argv[++i];
|
|
52
|
+
} else if (arg === "--policy") {
|
|
53
|
+
options.policy = argv[++i];
|
|
54
|
+
} else if (arg === "--force") {
|
|
55
|
+
options.force = true;
|
|
56
|
+
} else if (arg === "--no-source-scan") {
|
|
57
|
+
options.sourceScan = false;
|
|
58
|
+
} else if (arg === "--no-vulnerability-check") {
|
|
59
|
+
options.vulnerabilityCheck = false;
|
|
60
|
+
} else if (arg === "--reason") {
|
|
61
|
+
options.reason = true;
|
|
62
|
+
} else if (arg === "--reason-model") {
|
|
63
|
+
options.reasonModel = argv[++i];
|
|
64
|
+
} else if (arg === "--reason-provider") {
|
|
65
|
+
options.reasonProvider = argv[++i];
|
|
66
|
+
} else if (arg === "--reason-max-files") {
|
|
67
|
+
options.reasonMaxFiles = Number(argv[++i]);
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!["json", "markdown"].includes(options.format)) {
|
|
73
|
+
throw new Error("--format must be json or markdown");
|
|
74
|
+
}
|
|
75
|
+
return options;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readInput(file) {
|
|
79
|
+
if (file) {
|
|
80
|
+
return fs.readFileSync(file, "utf8");
|
|
81
|
+
}
|
|
82
|
+
return fs.readFileSync(0, "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function maybeReason(evidence, options) {
|
|
86
|
+
if (!options.reason) return null;
|
|
87
|
+
try {
|
|
88
|
+
return await reasonAbout(evidence, {
|
|
89
|
+
provider: options.reasonProvider,
|
|
90
|
+
model: options.reasonModel,
|
|
91
|
+
maxFiles: options.reasonMaxFiles
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return { error: { code: error.code || "REASONER_ERROR", message: error.message } };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function reasoningExitCode(reasoning) {
|
|
99
|
+
if (!reasoning || reasoning.error) return null;
|
|
100
|
+
if (reasoning.verdict === "block") return 2;
|
|
101
|
+
if (reasoning.verdict === "review") return 3;
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
const options = parseArgs(process.argv.slice(2));
|
|
107
|
+
if (options.help) {
|
|
108
|
+
printUsage();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.command === "guard") {
|
|
113
|
+
if (!options.reference) {
|
|
114
|
+
throw new Error("guard requires an extension reference");
|
|
115
|
+
}
|
|
116
|
+
const result = await guardExtension(options.reference, options);
|
|
117
|
+
if (options.reason) {
|
|
118
|
+
const evidenceForReason = {
|
|
119
|
+
packageName: result.resolved && result.resolved.packageName,
|
|
120
|
+
npmMetadata: result.resolved && result.resolved.npmMetadata,
|
|
121
|
+
githubMetadata: null,
|
|
122
|
+
webPresence: null,
|
|
123
|
+
sourceFiles: result.sourceFiles || {}
|
|
124
|
+
};
|
|
125
|
+
const reasoning = await maybeReason(evidenceForReason, options);
|
|
126
|
+
result.reasoning = reasoning;
|
|
127
|
+
if (reasoning && !reasoning.error && reasoning.verdict) {
|
|
128
|
+
result.decision = reasoning.verdict === "block"
|
|
129
|
+
? "block"
|
|
130
|
+
: reasoning.verdict === "safe"
|
|
131
|
+
? "allow"
|
|
132
|
+
: "review";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (options.format === "json") {
|
|
136
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
137
|
+
} else {
|
|
138
|
+
process.stdout.write(`${renderGuardMarkdown(result)}\n`);
|
|
139
|
+
}
|
|
140
|
+
process.exitCode = result.decision === "block" ? 2 : result.decision === "review" ? 3 : 0;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const raw = readInput(options.file).trim();
|
|
145
|
+
if (!raw) {
|
|
146
|
+
throw new Error("No evidence JSON provided");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const evidence = JSON.parse(raw);
|
|
150
|
+
const report = auditEvidence(evidence);
|
|
151
|
+
const reasoning = await maybeReason(evidence, options);
|
|
152
|
+
|
|
153
|
+
const payload = reasoning ? { report, reasoning } : report;
|
|
154
|
+
|
|
155
|
+
if (options.format === "json") {
|
|
156
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
157
|
+
} else if (reasoning) {
|
|
158
|
+
process.stdout.write(`${renderMarkdown(report)}\n\n---\n\n${renderReasoningMarkdown(reasoning)}\n`);
|
|
159
|
+
} else {
|
|
160
|
+
process.stdout.write(`${renderMarkdown(report)}\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const exitFromReason = reasoningExitCode(reasoning);
|
|
164
|
+
if (exitFromReason !== null) {
|
|
165
|
+
process.exitCode = exitFromReason;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderGuardMarkdown(result) {
|
|
170
|
+
const lines = [
|
|
171
|
+
`Decision: **${result.decision.toUpperCase()}**`,
|
|
172
|
+
`Reference: \`${result.reference}\``,
|
|
173
|
+
`Quarantine: \`${result.quarantinePath}\``,
|
|
174
|
+
""
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
if (result.promotedPath) {
|
|
178
|
+
lines.push(`Promoted to: \`${result.promotedPath}\``, "");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
lines.push(renderMarkdown(result.report));
|
|
182
|
+
|
|
183
|
+
if (result.reasoning) {
|
|
184
|
+
lines.push("", "---", "", renderReasoningMarkdown(result.reasoning));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renderReasoningMarkdown(reasoning) {
|
|
191
|
+
if (reasoning.error) {
|
|
192
|
+
return `Reasoning: **unavailable** (${reasoning.error.code}: ${reasoning.error.message})`;
|
|
193
|
+
}
|
|
194
|
+
const lines = [
|
|
195
|
+
`Reasoning verdict: **${(reasoning.verdict || "?").toUpperCase()}**`,
|
|
196
|
+
`Provider: \`${reasoning.provider || "?"}\` · Model: \`${reasoning.model}\` · latency: ${reasoning.latencyMs} ms`,
|
|
197
|
+
"",
|
|
198
|
+
reasoning.summary || "",
|
|
199
|
+
""
|
|
200
|
+
];
|
|
201
|
+
if (reasoning.usage) {
|
|
202
|
+
const u = reasoning.usage;
|
|
203
|
+
const parts = [
|
|
204
|
+
`in=${u.input_tokens ?? "?"}`,
|
|
205
|
+
`out=${u.output_tokens ?? "?"}`,
|
|
206
|
+
`cache_read=${u.cache_read_input_tokens ?? 0}`,
|
|
207
|
+
`cache_write=${u.cache_creation_input_tokens ?? 0}`
|
|
208
|
+
];
|
|
209
|
+
lines.push(`Tokens: ${parts.join(" · ")}`, "");
|
|
210
|
+
}
|
|
211
|
+
if (reasoning.findings && reasoning.findings.length > 0) {
|
|
212
|
+
lines.push("Findings:");
|
|
213
|
+
for (const finding of reasoning.findings) {
|
|
214
|
+
lines.push(
|
|
215
|
+
`- **${finding.severity.toUpperCase()} - ${finding.category}**: ${finding.reasoning}`,
|
|
216
|
+
` Evidence: \`${finding.evidence}\``
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
lines.push("");
|
|
220
|
+
} else {
|
|
221
|
+
lines.push("Findings: none reported.", "");
|
|
222
|
+
}
|
|
223
|
+
if (reasoning.evidenceGaps && reasoning.evidenceGaps.length > 0) {
|
|
224
|
+
lines.push("Evidence gaps:");
|
|
225
|
+
for (const gap of reasoning.evidenceGaps) {
|
|
226
|
+
lines.push(`- ${gap}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
main().catch((error) => {
|
|
234
|
+
process.stderr.write(`pkgxray: ${error.message}\n`);
|
|
235
|
+
process.exitCode = 1;
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
process.stderr.write(`pkgxray: ${error.message}\n`);
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
}
|