@inner-security/scan 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 +91 -0
- package/bundle/index.d.ts +185 -0
- package/bundle/index.js +847 -0
- package/bundle/inner-cli.js +995 -0
- package/bundle/scan-cli.js +808 -0
- package/package.json +66 -0
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/args.ts
|
|
4
|
+
var SHORT_ALIASES = {
|
|
5
|
+
h: "help"
|
|
6
|
+
};
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const out = { _: [], flags: {} };
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const tok = argv[i];
|
|
11
|
+
if (tok.startsWith("--")) {
|
|
12
|
+
const eq = tok.indexOf("=");
|
|
13
|
+
if (eq >= 0) {
|
|
14
|
+
out.flags[tok.slice(2, eq)] = tok.slice(eq + 1);
|
|
15
|
+
} else {
|
|
16
|
+
const key = tok.slice(2);
|
|
17
|
+
const next = argv[i + 1];
|
|
18
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
19
|
+
out.flags[key] = next;
|
|
20
|
+
i++;
|
|
21
|
+
} else {
|
|
22
|
+
out.flags[key] = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} else if (tok.startsWith("-") && tok.length > 1) {
|
|
26
|
+
for (const ch of tok.slice(1)) {
|
|
27
|
+
out.flags[SHORT_ALIASES[ch] ?? ch] = true;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
out._.push(tok);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function flagString(flags, key) {
|
|
36
|
+
const v = flags[key];
|
|
37
|
+
return typeof v === "string" ? v : void 0;
|
|
38
|
+
}
|
|
39
|
+
function flagBool(flags, key) {
|
|
40
|
+
return flags[key] === true || flags[key] === "true";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ../verdict-client/dist/client.js
|
|
44
|
+
var DEFAULT_API_URL = "http://localhost:8787";
|
|
45
|
+
function isPending(item) {
|
|
46
|
+
return item.action === "PENDING";
|
|
47
|
+
}
|
|
48
|
+
function isEnvelope(item) {
|
|
49
|
+
return !isPending(item);
|
|
50
|
+
}
|
|
51
|
+
function sleep(ms) {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
54
|
+
function stripTrailingSlash(u) {
|
|
55
|
+
return u.replace(/\/+$/, "");
|
|
56
|
+
}
|
|
57
|
+
var VerdictClient = class {
|
|
58
|
+
baseUrl;
|
|
59
|
+
apiKey;
|
|
60
|
+
timeoutMs;
|
|
61
|
+
maxPollAttempts;
|
|
62
|
+
maxRetries;
|
|
63
|
+
fetchImpl;
|
|
64
|
+
cache;
|
|
65
|
+
constructor(opts = {}) {
|
|
66
|
+
this.baseUrl = stripTrailingSlash(opts.baseUrl ?? process.env.INNER_API_URL ?? DEFAULT_API_URL);
|
|
67
|
+
this.apiKey = opts.apiKey ?? process.env.INNER_API_KEY;
|
|
68
|
+
this.timeoutMs = opts.timeoutMs ?? 1e4;
|
|
69
|
+
this.maxPollAttempts = opts.maxPollAttempts ?? 8;
|
|
70
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
71
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
72
|
+
this.cache = opts.cache;
|
|
73
|
+
if (typeof this.fetchImpl !== "function") {
|
|
74
|
+
throw new Error("No fetch implementation available (Node >= 18 required, or pass fetchImpl).");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
headers() {
|
|
78
|
+
const h = { "content-type": "application/json" };
|
|
79
|
+
if (this.apiKey)
|
|
80
|
+
h.authorization = `Bearer ${this.apiKey}`;
|
|
81
|
+
return h;
|
|
82
|
+
}
|
|
83
|
+
/** A single fetch with a per-request AbortController timeout. */
|
|
84
|
+
async fetchOnce(url, init2) {
|
|
85
|
+
const ctrl = new AbortController();
|
|
86
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
87
|
+
try {
|
|
88
|
+
return await this.fetchImpl(url, { ...init2, signal: ctrl.signal });
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* fetch with timeout + retries-with-backoff on transient failures (network
|
|
95
|
+
* error / timeout / 5xx). 404 is returned as-is (the poll path treats it as
|
|
96
|
+
* "not ready yet"); other 4xx are returned as-is for the caller to handle.
|
|
97
|
+
*/
|
|
98
|
+
async fetchWithRetry(url, init2) {
|
|
99
|
+
let lastErr;
|
|
100
|
+
let wait = 250;
|
|
101
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
102
|
+
try {
|
|
103
|
+
const res = await this.fetchOnce(url, init2);
|
|
104
|
+
if (res.status >= 500 && attempt < this.maxRetries) {
|
|
105
|
+
await sleep(wait);
|
|
106
|
+
wait = Math.min(wait * 2, 5e3);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
return res;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
lastErr = err;
|
|
112
|
+
if (attempt < this.maxRetries) {
|
|
113
|
+
await sleep(wait);
|
|
114
|
+
wait = Math.min(wait * 2, 5e3);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw lastErr instanceof Error ? lastErr : new Error(`Request to ${url} failed`);
|
|
120
|
+
}
|
|
121
|
+
/** Raw C1 batch call. Items may come back as VerdictEnvelope or PendingResponse. */
|
|
122
|
+
async getVerdicts(items) {
|
|
123
|
+
const body = { items };
|
|
124
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}/v1/verdict`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: this.headers(),
|
|
127
|
+
body: JSON.stringify(body)
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
throw new Error(`Verdict API POST /v1/verdict failed: ${res.status} ${res.statusText}`);
|
|
131
|
+
}
|
|
132
|
+
return await res.json();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Poll a PENDING job to resolution. Returns the resolved item (may still be
|
|
136
|
+
* PENDING if it never resolves within maxPollAttempts). 404 is treated as
|
|
137
|
+
* "not ready yet" and retried; a non-ok, non-404 response throws.
|
|
138
|
+
*/
|
|
139
|
+
async pollVerdict(pending) {
|
|
140
|
+
let attempt = 0;
|
|
141
|
+
let waitMs = Math.max(250, pending.eta_ms || 1e3);
|
|
142
|
+
let last = pending;
|
|
143
|
+
const url = `${this.baseUrl}/v1/verdict/${encodeURIComponent(pending.job_id)}`;
|
|
144
|
+
while (attempt < this.maxPollAttempts) {
|
|
145
|
+
await sleep(Math.min(waitMs, 5e3));
|
|
146
|
+
const res = await this.fetchWithRetry(url, {
|
|
147
|
+
method: "GET",
|
|
148
|
+
headers: this.headers()
|
|
149
|
+
});
|
|
150
|
+
if (res.status === 404) {
|
|
151
|
+
attempt += 1;
|
|
152
|
+
waitMs = Math.min(waitMs * 2, 5e3);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
throw new Error(`Verdict API GET /v1/verdict/${pending.job_id} failed: ${res.status} ${res.statusText}`);
|
|
157
|
+
}
|
|
158
|
+
last = await res.json();
|
|
159
|
+
if (!isPending(last))
|
|
160
|
+
return last;
|
|
161
|
+
attempt += 1;
|
|
162
|
+
waitMs = Math.min(waitMs * 2, 5e3);
|
|
163
|
+
}
|
|
164
|
+
return last;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a single item to a final envelope, polling through PENDING. Returns
|
|
168
|
+
* the envelope, or the unresolved PendingResponse if it never landed.
|
|
169
|
+
*/
|
|
170
|
+
async resolveVerdict(item) {
|
|
171
|
+
const { results } = await this.getVerdicts([item]);
|
|
172
|
+
const first = results[0];
|
|
173
|
+
if (!first)
|
|
174
|
+
throw new Error("Verdict API returned no results for the request.");
|
|
175
|
+
if (isPending(first))
|
|
176
|
+
return this.pollVerdict(first);
|
|
177
|
+
return first;
|
|
178
|
+
}
|
|
179
|
+
/** Resolve a batch, polling each PENDING to resolution. Order matches input. */
|
|
180
|
+
async resolveVerdicts(items) {
|
|
181
|
+
const { results } = await this.getVerdicts(items);
|
|
182
|
+
return Promise.all(results.map((r) => isPending(r) ? this.pollVerdict(r) : Promise.resolve(r)));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* High-level fail-open resolution (the CLI scan path). Returns one
|
|
186
|
+
* ResolvedItem per requested item. On a total API failure (network/timeout),
|
|
187
|
+
* falls back to the fail-open cache for every item; for individual
|
|
188
|
+
* missing/never-resolved items, falls back to the cache too. Items the cache
|
|
189
|
+
* cannot safely satisfy surface as 'unresolved' (UNKNOWN) so the operator
|
|
190
|
+
* decides — never a silent allow.
|
|
191
|
+
*/
|
|
192
|
+
async resolve(items) {
|
|
193
|
+
if (items.length === 0)
|
|
194
|
+
return { resolved: [], degraded: false };
|
|
195
|
+
let response;
|
|
196
|
+
try {
|
|
197
|
+
response = await this.getVerdicts(items);
|
|
198
|
+
} catch {
|
|
199
|
+
return this.failOpen(items);
|
|
200
|
+
}
|
|
201
|
+
const results = response.results ?? [];
|
|
202
|
+
const resolved = [];
|
|
203
|
+
for (let i = 0; i < items.length; i++) {
|
|
204
|
+
const item = items[i];
|
|
205
|
+
const r = results[i];
|
|
206
|
+
if (!r) {
|
|
207
|
+
resolved.push(this.fromCacheOrUnresolved(item));
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (isPending(r)) {
|
|
211
|
+
let env;
|
|
212
|
+
try {
|
|
213
|
+
env = await this.pollVerdict(r);
|
|
214
|
+
} catch {
|
|
215
|
+
env = void 0;
|
|
216
|
+
}
|
|
217
|
+
if (env && isEnvelope(env)) {
|
|
218
|
+
this.cache?.put(item, env);
|
|
219
|
+
resolved.push({ item, envelope: env, source: "api" });
|
|
220
|
+
} else {
|
|
221
|
+
resolved.push(this.fromCacheOrUnresolved(item));
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
this.cache?.put(item, r);
|
|
226
|
+
resolved.push({ item, envelope: r, source: "api" });
|
|
227
|
+
}
|
|
228
|
+
return { resolved, degraded: false };
|
|
229
|
+
}
|
|
230
|
+
failOpen(items) {
|
|
231
|
+
const resolved = items.map((item) => {
|
|
232
|
+
const cached = this.cache?.get(item);
|
|
233
|
+
if (cached) {
|
|
234
|
+
return { item, envelope: cached, source: "cache", failedOpen: true };
|
|
235
|
+
}
|
|
236
|
+
return { item, source: "unresolved", failedOpen: true };
|
|
237
|
+
});
|
|
238
|
+
return { resolved, degraded: true };
|
|
239
|
+
}
|
|
240
|
+
fromCacheOrUnresolved(item) {
|
|
241
|
+
const cached = this.cache?.get(item);
|
|
242
|
+
if (cached)
|
|
243
|
+
return { item, envelope: cached, source: "cache" };
|
|
244
|
+
return { item, source: "unresolved" };
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// src/config.ts
|
|
249
|
+
var DEFAULT_PROXY_URL = "https://proxy.inner.dev";
|
|
250
|
+
function stripTrailingSlash2(u) {
|
|
251
|
+
return u.replace(/\/+$/, "");
|
|
252
|
+
}
|
|
253
|
+
function resolveApiUrl(flagValue) {
|
|
254
|
+
const fromFlag = flagValue?.trim();
|
|
255
|
+
const fromEnv = process.env.INNER_API_URL?.trim();
|
|
256
|
+
const chosen = fromFlag || fromEnv || DEFAULT_API_URL;
|
|
257
|
+
return { url: stripTrailingSlash2(chosen), isDefault: !fromFlag && !fromEnv };
|
|
258
|
+
}
|
|
259
|
+
function resolveProxyUrl(flagValue) {
|
|
260
|
+
const fromFlag = flagValue?.trim();
|
|
261
|
+
const fromEnv = process.env.INNER_PROXY_URL?.trim();
|
|
262
|
+
const chosen = fromFlag || fromEnv || DEFAULT_PROXY_URL;
|
|
263
|
+
return { url: stripTrailingSlash2(chosen), isDefault: !fromFlag && !fromEnv };
|
|
264
|
+
}
|
|
265
|
+
function resolveConfig(opts = {}) {
|
|
266
|
+
const api = resolveApiUrl(opts.apiUrl);
|
|
267
|
+
const proxy = resolveProxyUrl(opts.proxyUrl);
|
|
268
|
+
return {
|
|
269
|
+
apiUrl: api.url,
|
|
270
|
+
proxyUrl: proxy.url,
|
|
271
|
+
apiUrlIsDefault: api.isDefault,
|
|
272
|
+
proxyUrlIsDefault: proxy.isDefault
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/manifest.ts
|
|
277
|
+
import { existsSync, readFileSync } from "fs";
|
|
278
|
+
import { join } from "path";
|
|
279
|
+
function uniqueItems(items) {
|
|
280
|
+
const seen = /* @__PURE__ */ new Set();
|
|
281
|
+
const out = [];
|
|
282
|
+
for (const it of items) {
|
|
283
|
+
const k = `${it.ecosystem}:${it.name}:${it.version}`;
|
|
284
|
+
if (seen.has(k)) continue;
|
|
285
|
+
seen.add(k);
|
|
286
|
+
out.push(it);
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
function cleanVersion(raw) {
|
|
291
|
+
const trimmed = raw.trim();
|
|
292
|
+
if (!trimmed) return null;
|
|
293
|
+
if (trimmed === "*" || trimmed === "latest" || trimmed === "x" || trimmed === "X") return null;
|
|
294
|
+
const cleaned = trimmed.replace(/^[\^~>=<\s]+/, "").replace(/\s.*$/, "").replace(/^v/, "").trim();
|
|
295
|
+
if (!/^\d/.test(cleaned)) return null;
|
|
296
|
+
return cleaned;
|
|
297
|
+
}
|
|
298
|
+
function readNpmFromPackageJson(root) {
|
|
299
|
+
const p = join(root, "package.json");
|
|
300
|
+
if (!existsSync(p)) return { items: [], warnings: [] };
|
|
301
|
+
const items = [];
|
|
302
|
+
const warnings = [];
|
|
303
|
+
try {
|
|
304
|
+
const pkg = JSON.parse(readFileSync(p, "utf8"));
|
|
305
|
+
for (const block of [pkg.dependencies, pkg.devDependencies, pkg.optionalDependencies]) {
|
|
306
|
+
if (!block) continue;
|
|
307
|
+
for (const [name, spec] of Object.entries(block)) {
|
|
308
|
+
if (/^(workspace:|link:|file:|git\+|git:|github:|https?:)/.test(spec)) {
|
|
309
|
+
warnings.push(`package.json: skipped "${name}" (${spec}) \u2014 not registry-resolvable.`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const alias = spec.match(/^npm:(@?[^@]+(?:\/[^@]+)?)@(.+)$/);
|
|
313
|
+
if (alias) {
|
|
314
|
+
const aliasName = alias[1];
|
|
315
|
+
const aliasVersion = cleanVersion(alias[2]);
|
|
316
|
+
if (!aliasVersion) {
|
|
317
|
+
warnings.push(`package.json: skipped "${name}" (${spec}) \u2014 alias has no concrete version.`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
items.push({ ecosystem: "npm", name: aliasName, version: aliasVersion });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const version = cleanVersion(spec);
|
|
324
|
+
if (!version) {
|
|
325
|
+
warnings.push(`package.json: skipped "${name}" (${spec}) \u2014 no concrete version (wildcard/tag/range).`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
items.push({ ecosystem: "npm", name, version });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
return { items, warnings };
|
|
334
|
+
}
|
|
335
|
+
function readNpmFromPackageLock(root) {
|
|
336
|
+
const p = join(root, "package-lock.json");
|
|
337
|
+
if (!existsSync(p)) return { items: [], warnings: [] };
|
|
338
|
+
const items = [];
|
|
339
|
+
try {
|
|
340
|
+
const lock = JSON.parse(readFileSync(p, "utf8"));
|
|
341
|
+
if (lock.packages) {
|
|
342
|
+
for (const [path, meta] of Object.entries(lock.packages)) {
|
|
343
|
+
if (!path.startsWith("node_modules/")) continue;
|
|
344
|
+
const name = path.slice(path.lastIndexOf("node_modules/") + "node_modules/".length);
|
|
345
|
+
if (!meta.version) continue;
|
|
346
|
+
items.push({ ecosystem: "npm", name, version: meta.version });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (lock.dependencies && items.length === 0) {
|
|
350
|
+
for (const [name, meta] of Object.entries(lock.dependencies)) {
|
|
351
|
+
if (!meta.version) continue;
|
|
352
|
+
items.push({ ecosystem: "npm", name, version: meta.version });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
return { items, warnings: [] };
|
|
358
|
+
}
|
|
359
|
+
function readNpmFromYarnLock(root) {
|
|
360
|
+
const p = join(root, "yarn.lock");
|
|
361
|
+
if (!existsSync(p)) return { items: [], warnings: [] };
|
|
362
|
+
const items = [];
|
|
363
|
+
try {
|
|
364
|
+
const text = readFileSync(p, "utf8");
|
|
365
|
+
const lines = text.split(/\r?\n/);
|
|
366
|
+
let pendingNames = [];
|
|
367
|
+
for (const line of lines) {
|
|
368
|
+
if (/^[^\s].*:\s*$/.test(line) && !line.startsWith("#")) {
|
|
369
|
+
pendingNames = parseYarnHeaderNames(line.replace(/:\s*$/, ""));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const m = line.match(/^\s+version:?\s+"?([^"\s]+)"?/);
|
|
373
|
+
if (m && pendingNames.length) {
|
|
374
|
+
const version = m[1];
|
|
375
|
+
for (const name of pendingNames) items.push({ ecosystem: "npm", name, version });
|
|
376
|
+
pendingNames = [];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
return { items, warnings: [] };
|
|
382
|
+
}
|
|
383
|
+
function parseYarnHeaderNames(header) {
|
|
384
|
+
const names = /* @__PURE__ */ new Set();
|
|
385
|
+
for (let part of header.split(",")) {
|
|
386
|
+
part = part.trim().replace(/^"|"$/g, "");
|
|
387
|
+
if (!part) continue;
|
|
388
|
+
const at = part.lastIndexOf("@");
|
|
389
|
+
if (at <= 0) continue;
|
|
390
|
+
names.add(part.slice(0, at));
|
|
391
|
+
}
|
|
392
|
+
return [...names];
|
|
393
|
+
}
|
|
394
|
+
function readNpmFromPnpmLock(root) {
|
|
395
|
+
const p = join(root, "pnpm-lock.yaml");
|
|
396
|
+
if (!existsSync(p)) return { items: [], warnings: [] };
|
|
397
|
+
const items = [];
|
|
398
|
+
try {
|
|
399
|
+
const text = readFileSync(p, "utf8");
|
|
400
|
+
const re = /^\s*['"]?\/?((?:@[^/@\s]+\/)?[^/@\s'"]+)@(\d[^()'":\s]*)(?:\([^)]*\))*['"]?\s*:/gm;
|
|
401
|
+
let m;
|
|
402
|
+
while ((m = re.exec(text)) !== null) {
|
|
403
|
+
items.push({ ecosystem: "npm", name: m[1], version: m[2] });
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
return { items, warnings: [] };
|
|
408
|
+
}
|
|
409
|
+
function readPypiFromRequirements(root) {
|
|
410
|
+
const items = [];
|
|
411
|
+
const warnings = [];
|
|
412
|
+
for (const file of ["requirements.txt"]) {
|
|
413
|
+
const p = join(root, file);
|
|
414
|
+
if (!existsSync(p)) continue;
|
|
415
|
+
try {
|
|
416
|
+
const text = readFileSync(p, "utf8");
|
|
417
|
+
for (let line of text.split(/\r?\n/)) {
|
|
418
|
+
line = line.replace(/\s+#.*$/, "").trim();
|
|
419
|
+
if (!line || line.startsWith("#")) continue;
|
|
420
|
+
if (line.startsWith("-") || line.includes("://")) {
|
|
421
|
+
warnings.push(`requirements.txt: skipped "${line.slice(0, 60)}" \u2014 not a simple pin.`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const m = line.match(/^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?\s*([=<>!~]=?)\s*([0-9][^\s;#]*)/);
|
|
425
|
+
if (m) {
|
|
426
|
+
items.push({ ecosystem: "pypi", name: m[1], version: m[3] });
|
|
427
|
+
} else {
|
|
428
|
+
warnings.push(`requirements.txt: skipped "${line.slice(0, 60)}" \u2014 no concrete version.`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { items, warnings };
|
|
435
|
+
}
|
|
436
|
+
function readPypiFromPoetryLock(root) {
|
|
437
|
+
const p = join(root, "poetry.lock");
|
|
438
|
+
if (!existsSync(p)) return { items: [], warnings: [] };
|
|
439
|
+
const items = [];
|
|
440
|
+
try {
|
|
441
|
+
const text = readFileSync(p, "utf8");
|
|
442
|
+
const blocks = text.split(/\[\[package\]\]/);
|
|
443
|
+
for (const block of blocks) {
|
|
444
|
+
const name = block.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
|
|
445
|
+
const version = block.match(/^\s*version\s*=\s*"([^"]+)"/m)?.[1];
|
|
446
|
+
if (name && version) items.push({ ecosystem: "pypi", name, version });
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
return { items, warnings: [] };
|
|
451
|
+
}
|
|
452
|
+
function readPypiFromUvLock(root) {
|
|
453
|
+
const p = join(root, "uv.lock");
|
|
454
|
+
if (!existsSync(p)) return { items: [], warnings: [] };
|
|
455
|
+
const items = [];
|
|
456
|
+
try {
|
|
457
|
+
const text = readFileSync(p, "utf8");
|
|
458
|
+
const blocks = text.split(/\[\[package\]\]/);
|
|
459
|
+
for (const block of blocks) {
|
|
460
|
+
const name = block.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
|
|
461
|
+
const version = block.match(/^\s*version\s*=\s*"([^"]+)"/m)?.[1];
|
|
462
|
+
if (name && version) items.push({ ecosystem: "pypi", name, version });
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
return { items, warnings: [] };
|
|
467
|
+
}
|
|
468
|
+
function readPypiFromPyproject(root) {
|
|
469
|
+
const p = join(root, "pyproject.toml");
|
|
470
|
+
if (!existsSync(p)) return { items: [], warnings: [] };
|
|
471
|
+
const items = [];
|
|
472
|
+
const warnings = [];
|
|
473
|
+
try {
|
|
474
|
+
const text = readFileSync(p, "utf8");
|
|
475
|
+
const arr = text.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
|
|
476
|
+
if (arr) {
|
|
477
|
+
for (const m of arr[1].matchAll(/"([^"]+)"/g)) {
|
|
478
|
+
const spec = m[1];
|
|
479
|
+
const dm = spec.match(/^([A-Za-z0-9._-]+)(?:\[[^\]]*\])?\s*([=<>!~]=?)\s*([0-9][^\s;]*)/);
|
|
480
|
+
if (dm) items.push({ ecosystem: "pypi", name: dm[1], version: dm[3] });
|
|
481
|
+
else warnings.push(`pyproject.toml: skipped "${spec.slice(0, 60)}" \u2014 no concrete version.`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
return { items, warnings };
|
|
487
|
+
}
|
|
488
|
+
function scanManifests(root = process.cwd()) {
|
|
489
|
+
const items = [];
|
|
490
|
+
const sources = [];
|
|
491
|
+
const ecosystems = /* @__PURE__ */ new Set();
|
|
492
|
+
const warnings = [];
|
|
493
|
+
const npmReaders = [
|
|
494
|
+
["package-lock.json", readNpmFromPackageLock],
|
|
495
|
+
["pnpm-lock.yaml", readNpmFromPnpmLock],
|
|
496
|
+
["yarn.lock", readNpmFromYarnLock],
|
|
497
|
+
["package.json", readNpmFromPackageJson]
|
|
498
|
+
];
|
|
499
|
+
for (const [file, reader] of npmReaders) {
|
|
500
|
+
if (!existsSync(join(root, file))) continue;
|
|
501
|
+
const got = reader(root);
|
|
502
|
+
if (got.items.length) {
|
|
503
|
+
items.push(...got.items);
|
|
504
|
+
warnings.push(...got.warnings);
|
|
505
|
+
sources.push(file);
|
|
506
|
+
ecosystems.add("npm");
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const pypiReaders = [
|
|
511
|
+
["poetry.lock", readPypiFromPoetryLock],
|
|
512
|
+
["uv.lock", readPypiFromUvLock],
|
|
513
|
+
["requirements.txt", readPypiFromRequirements],
|
|
514
|
+
["pyproject.toml", readPypiFromPyproject]
|
|
515
|
+
];
|
|
516
|
+
for (const [file, reader] of pypiReaders) {
|
|
517
|
+
if (!existsSync(join(root, file))) continue;
|
|
518
|
+
const got = reader(root);
|
|
519
|
+
if (got.items.length) {
|
|
520
|
+
items.push(...got.items);
|
|
521
|
+
warnings.push(...got.warnings);
|
|
522
|
+
sources.push(file);
|
|
523
|
+
ecosystems.add("pypi");
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
items: uniqueItems(items),
|
|
529
|
+
sources,
|
|
530
|
+
ecosystems: [...ecosystems],
|
|
531
|
+
warnings,
|
|
532
|
+
skipped: warnings.length
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/cache.ts
|
|
537
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
538
|
+
import { join as join2 } from "path";
|
|
539
|
+
var CACHE_DIR_NAME = ".inner-cache";
|
|
540
|
+
var CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
541
|
+
function cacheKey(item) {
|
|
542
|
+
return `${item.ecosystem}:${item.name}:${item.version}`;
|
|
543
|
+
}
|
|
544
|
+
function keyToFilename(key) {
|
|
545
|
+
return key.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json";
|
|
546
|
+
}
|
|
547
|
+
var VerdictCache = class {
|
|
548
|
+
dir;
|
|
549
|
+
constructor(rootDir = process.cwd()) {
|
|
550
|
+
this.dir = join2(rootDir, CACHE_DIR_NAME);
|
|
551
|
+
}
|
|
552
|
+
pathFor(key) {
|
|
553
|
+
return join2(this.dir, keyToFilename(key));
|
|
554
|
+
}
|
|
555
|
+
/** Persist a resolved envelope as last-known-good. */
|
|
556
|
+
put(item, envelope) {
|
|
557
|
+
try {
|
|
558
|
+
if (!existsSync2(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
559
|
+
const entry = {
|
|
560
|
+
key: cacheKey(item),
|
|
561
|
+
cached_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
562
|
+
envelope
|
|
563
|
+
};
|
|
564
|
+
writeFileSync(this.pathFor(entry.key), JSON.stringify(entry, null, 2), "utf8");
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Read a last-known-good envelope that is safe to TRUST for resolution, or
|
|
570
|
+
* undefined on miss/parse error/untrusted entry.
|
|
571
|
+
*
|
|
572
|
+
* The cache is fail-OPEN: only a cached ALLOW that has not gone stale is a
|
|
573
|
+
* usable verdict. A cached BLOCK/REVIEW (or anything non-ALLOW), or an
|
|
574
|
+
* expired entry, is treated as a miss so the caller surfaces UNKNOWN and the
|
|
575
|
+
* operator decides — never a silent allow of a now-known-bad package.
|
|
576
|
+
*/
|
|
577
|
+
get(item, now = /* @__PURE__ */ new Date()) {
|
|
578
|
+
try {
|
|
579
|
+
const p = this.pathFor(cacheKey(item));
|
|
580
|
+
if (!existsSync2(p)) return void 0;
|
|
581
|
+
const entry = JSON.parse(readFileSync2(p, "utf8"));
|
|
582
|
+
const envelope = entry.envelope;
|
|
583
|
+
if (!envelope || envelope.action !== "ALLOW") return void 0;
|
|
584
|
+
if (isStale(entry, now)) return void 0;
|
|
585
|
+
return envelope;
|
|
586
|
+
} catch {
|
|
587
|
+
return void 0;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
function isStale(entry, now) {
|
|
592
|
+
const nowMs = now.getTime();
|
|
593
|
+
const expiresMs = Date.parse(entry.envelope?.expires_at ?? "");
|
|
594
|
+
if (!Number.isNaN(expiresMs)) return expiresMs <= nowMs;
|
|
595
|
+
const cachedMs = Date.parse(entry.cached_at ?? "");
|
|
596
|
+
if (!Number.isNaN(cachedMs)) return nowMs - cachedMs >= CACHE_MAX_AGE_MS;
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/client.ts
|
|
601
|
+
var VerdictClient2 = class {
|
|
602
|
+
client;
|
|
603
|
+
constructor(opts) {
|
|
604
|
+
const cache = opts.noCache ? void 0 : new VerdictCache(opts.cacheRoot);
|
|
605
|
+
this.client = new VerdictClient({
|
|
606
|
+
baseUrl: opts.apiUrl,
|
|
607
|
+
timeoutMs: opts.timeoutMs,
|
|
608
|
+
maxPollAttempts: opts.maxPollAttempts ?? 5,
|
|
609
|
+
// The CLI scan gate fails open FAST: no transient-retry before serving the
|
|
610
|
+
// last-known-good cache (CR-E1.1). A build must not hang on a slow/hung
|
|
611
|
+
// Verdict API. The shared client keeps retries for the MCP path, which has
|
|
612
|
+
// no build-latency contract and benefits from riding transient blips.
|
|
613
|
+
maxRetries: 0,
|
|
614
|
+
cache: cache ? {
|
|
615
|
+
get: (item) => cache.get(item),
|
|
616
|
+
put: (item, env) => cache.put(item, env)
|
|
617
|
+
} : void 0
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Resolve a batch of items. Returns one ResolvedItem per requested item.
|
|
622
|
+
* On total API failure, falls back to the fail-open cache for every item.
|
|
623
|
+
*/
|
|
624
|
+
resolve(items) {
|
|
625
|
+
return this.client.resolve(items);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// src/scan.ts
|
|
630
|
+
function aggregate(resolved) {
|
|
631
|
+
const tally = {
|
|
632
|
+
scanned: resolved.length,
|
|
633
|
+
allowed: 0,
|
|
634
|
+
blocked: 0,
|
|
635
|
+
review: 0,
|
|
636
|
+
unknown: 0,
|
|
637
|
+
fromCache: 0
|
|
638
|
+
};
|
|
639
|
+
for (const r of resolved) {
|
|
640
|
+
if (r.source === "cache") tally.fromCache++;
|
|
641
|
+
if (!r.envelope) {
|
|
642
|
+
tally.unknown++;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const action = r.envelope.action;
|
|
646
|
+
switch (action) {
|
|
647
|
+
case "ALLOW":
|
|
648
|
+
tally.allowed++;
|
|
649
|
+
break;
|
|
650
|
+
case "BLOCK":
|
|
651
|
+
tally.blocked++;
|
|
652
|
+
break;
|
|
653
|
+
case "REVIEW":
|
|
654
|
+
tally.review++;
|
|
655
|
+
break;
|
|
656
|
+
default:
|
|
657
|
+
tally.unknown++;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return tally;
|
|
661
|
+
}
|
|
662
|
+
async function scan(opts = {}) {
|
|
663
|
+
const root = opts.root ?? process.cwd();
|
|
664
|
+
const cfg = resolveConfig({ apiUrl: opts.apiUrl });
|
|
665
|
+
const manifest = opts.manifest ?? scanManifests(root);
|
|
666
|
+
const client = new VerdictClient2({
|
|
667
|
+
apiUrl: cfg.apiUrl,
|
|
668
|
+
cacheRoot: root,
|
|
669
|
+
noCache: opts.noCache
|
|
670
|
+
});
|
|
671
|
+
const { resolved, degraded } = await client.resolve(manifest.items);
|
|
672
|
+
return {
|
|
673
|
+
tally: aggregate(resolved),
|
|
674
|
+
resolved,
|
|
675
|
+
sources: manifest.sources,
|
|
676
|
+
ecosystems: manifest.ecosystems,
|
|
677
|
+
degraded,
|
|
678
|
+
apiUrl: cfg.apiUrl,
|
|
679
|
+
apiUrlIsDefault: cfg.apiUrlIsDefault,
|
|
680
|
+
skipped: manifest.skipped,
|
|
681
|
+
warnings: manifest.warnings
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function formatTally(t) {
|
|
685
|
+
return `${t.scanned} packages scanned, ${t.allowed} allowed, ${t.blocked} blocked`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/scan-render.ts
|
|
689
|
+
async function runScanCommand(opts) {
|
|
690
|
+
const result = await scan(opts);
|
|
691
|
+
if (opts.json) return writeJson(result);
|
|
692
|
+
return writeText(result);
|
|
693
|
+
}
|
|
694
|
+
function writeJson(result) {
|
|
695
|
+
process.stdout.write(
|
|
696
|
+
JSON.stringify(
|
|
697
|
+
{
|
|
698
|
+
tally: result.tally,
|
|
699
|
+
ecosystems: result.ecosystems,
|
|
700
|
+
sources: result.sources,
|
|
701
|
+
degraded: result.degraded,
|
|
702
|
+
apiUrl: result.apiUrl,
|
|
703
|
+
apiUrlIsDefault: result.apiUrlIsDefault,
|
|
704
|
+
skipped: result.skipped,
|
|
705
|
+
warnings: result.warnings,
|
|
706
|
+
items: result.resolved.map((r) => ({
|
|
707
|
+
ecosystem: r.item.ecosystem,
|
|
708
|
+
name: r.item.name,
|
|
709
|
+
version: r.item.version,
|
|
710
|
+
action: r.envelope?.action ?? "UNKNOWN",
|
|
711
|
+
status: r.envelope?.status,
|
|
712
|
+
source: r.source
|
|
713
|
+
}))
|
|
714
|
+
},
|
|
715
|
+
null,
|
|
716
|
+
2
|
|
717
|
+
) + "\n"
|
|
718
|
+
);
|
|
719
|
+
return result.tally.blocked > 0 ? 1 : 0;
|
|
720
|
+
}
|
|
721
|
+
function writeText(result) {
|
|
722
|
+
const t = result.tally;
|
|
723
|
+
if (t.scanned === 0) {
|
|
724
|
+
process.stdout.write(
|
|
725
|
+
"No package manifest/lockfile found. Run inside a project with a package.json / requirements.txt / lockfile.\n"
|
|
726
|
+
);
|
|
727
|
+
return 0;
|
|
728
|
+
}
|
|
729
|
+
process.stdout.write("Inner scan \u2014 " + (result.ecosystems.join(", ") || "no ecosystem") + "\n");
|
|
730
|
+
if (result.sources.length) {
|
|
731
|
+
process.stdout.write(" sources: " + result.sources.join(", ") + "\n");
|
|
732
|
+
}
|
|
733
|
+
process.stdout.write(" " + formatTally(t) + "\n");
|
|
734
|
+
if (t.review) process.stdout.write(` ${t.review} need review
|
|
735
|
+
`);
|
|
736
|
+
if (t.unknown) process.stdout.write(` ${t.unknown} unknown / unresolved
|
|
737
|
+
`);
|
|
738
|
+
if (result.skipped) {
|
|
739
|
+
process.stdout.write(
|
|
740
|
+
` \u26A0 ${result.skipped} manifest entr${result.skipped === 1 ? "y" : "ies"} skipped (unparseable \u2014 not scanned):
|
|
741
|
+
`
|
|
742
|
+
);
|
|
743
|
+
for (const w of result.warnings) process.stdout.write(` - ${w}
|
|
744
|
+
`);
|
|
745
|
+
}
|
|
746
|
+
if (result.degraded) {
|
|
747
|
+
process.stdout.write(
|
|
748
|
+
` (API unreachable \u2014 ${t.fromCache} resolved from fail-open cache, others marked unknown)
|
|
749
|
+
`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
if (result.apiUrlIsDefault) {
|
|
753
|
+
process.stdout.write(
|
|
754
|
+
` note: using default Verdict API ${result.apiUrl} \u2014 set INNER_API_URL to the production endpoint.
|
|
755
|
+
`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
const blocked = result.resolved.filter((r) => r.envelope?.action === "BLOCK");
|
|
759
|
+
if (blocked.length) {
|
|
760
|
+
process.stdout.write("\nBLOCKED:\n");
|
|
761
|
+
for (const b of blocked) {
|
|
762
|
+
process.stdout.write(
|
|
763
|
+
` \u2717 ${b.item.ecosystem}:${b.item.name}@${b.item.version}` + (b.envelope?.status ? ` (${b.envelope.status})` : "") + "\n"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return t.blocked > 0 ? 1 : 0;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/init.ts
|
|
771
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
772
|
+
import { join as join3 } from "path";
|
|
773
|
+
var ALL_PACKAGE_MANAGERS = [
|
|
774
|
+
"npm",
|
|
775
|
+
"pnpm",
|
|
776
|
+
"yarn",
|
|
777
|
+
"pip",
|
|
778
|
+
"poetry",
|
|
779
|
+
"uv"
|
|
780
|
+
];
|
|
781
|
+
function npmRegistry(proxy) {
|
|
782
|
+
return `${proxy}/npm/`;
|
|
783
|
+
}
|
|
784
|
+
function pypiSimpleIndex(proxy) {
|
|
785
|
+
return `${proxy}/pypi/simple/`;
|
|
786
|
+
}
|
|
787
|
+
function upsertLine(body, key, line) {
|
|
788
|
+
const lines = body.length ? body.split(/\r?\n/) : [];
|
|
789
|
+
const idx = lines.findIndex((l) => l.trim().startsWith(`${key}=`) || l.trim().startsWith(`${key} =`));
|
|
790
|
+
if (idx >= 0) {
|
|
791
|
+
lines[idx] = line;
|
|
792
|
+
} else {
|
|
793
|
+
if (lines.length && lines[lines.length - 1].trim() !== "") lines.push(line);
|
|
794
|
+
else if (lines.length) lines[lines.length - 1] = line;
|
|
795
|
+
else lines.push(line);
|
|
796
|
+
}
|
|
797
|
+
return lines.join("\n").replace(/\n*$/, "\n");
|
|
798
|
+
}
|
|
799
|
+
function writeConfig(manager, file, registry, next) {
|
|
800
|
+
const existed = existsSync3(file);
|
|
801
|
+
writeFileSync2(file, next, "utf8");
|
|
802
|
+
return { manager, file, registry, outcome: existed ? "updated" : "created" };
|
|
803
|
+
}
|
|
804
|
+
function configureNpm(root, proxy) {
|
|
805
|
+
const file = join3(root, ".npmrc");
|
|
806
|
+
const registry = npmRegistry(proxy);
|
|
807
|
+
const body = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
808
|
+
const next = upsertLine(body, "registry", `registry=${registry}`);
|
|
809
|
+
return writeConfig("npm", file, registry, next);
|
|
810
|
+
}
|
|
811
|
+
function configurePnpm(root, proxy) {
|
|
812
|
+
const file = join3(root, ".npmrc");
|
|
813
|
+
const registry = npmRegistry(proxy);
|
|
814
|
+
const body = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
815
|
+
const next = upsertLine(body, "registry", `registry=${registry}`);
|
|
816
|
+
const res = writeConfig("pnpm", file, registry, next);
|
|
817
|
+
return res;
|
|
818
|
+
}
|
|
819
|
+
function configureYarn(root, proxy) {
|
|
820
|
+
const file = join3(root, ".yarnrc.yml");
|
|
821
|
+
const registry = npmRegistry(proxy);
|
|
822
|
+
let body = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
823
|
+
const line = `npmRegistryServer: "${registry}"`;
|
|
824
|
+
if (/^npmRegistryServer:/m.test(body)) {
|
|
825
|
+
body = body.replace(/^npmRegistryServer:.*$/m, line);
|
|
826
|
+
} else {
|
|
827
|
+
body = (body.replace(/\n*$/, "") + (body ? "\n" : "") + line).replace(/^\n/, "");
|
|
828
|
+
}
|
|
829
|
+
body = body.replace(/\n*$/, "\n");
|
|
830
|
+
return writeConfig("yarn", file, registry, body);
|
|
831
|
+
}
|
|
832
|
+
function configurePip(root, proxy) {
|
|
833
|
+
const file = join3(root, "pip.conf");
|
|
834
|
+
const registry = pypiSimpleIndex(proxy);
|
|
835
|
+
let body = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
836
|
+
if (!/^\[global\]/m.test(body)) {
|
|
837
|
+
body = (body.replace(/\n*$/, "") + (body ? "\n\n" : "") + "[global]\n").replace(/^\n+/, "");
|
|
838
|
+
}
|
|
839
|
+
if (/^index-url\s*=/m.test(body)) {
|
|
840
|
+
body = body.replace(/^index-url\s*=.*$/m, `index-url = ${registry}`);
|
|
841
|
+
} else {
|
|
842
|
+
body = body.replace(/^\[global\]\s*$/m, `[global]
|
|
843
|
+
index-url = ${registry}`);
|
|
844
|
+
}
|
|
845
|
+
body = body.replace(/\n*$/, "\n");
|
|
846
|
+
return writeConfig("pip", file, registry, body);
|
|
847
|
+
}
|
|
848
|
+
function configurePoetry(root, proxy) {
|
|
849
|
+
const file = join3(root, "poetry.toml");
|
|
850
|
+
const registry = pypiSimpleIndex(proxy);
|
|
851
|
+
const block = [
|
|
852
|
+
"[[tool.poetry.source]]",
|
|
853
|
+
'name = "inner"',
|
|
854
|
+
`url = "${registry}"`,
|
|
855
|
+
'priority = "primary"',
|
|
856
|
+
""
|
|
857
|
+
].join("\n");
|
|
858
|
+
let body = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
859
|
+
if (/\[\[tool\.poetry\.source\]\][\s\S]*?name\s*=\s*"inner"/.test(body)) {
|
|
860
|
+
body = body.replace(
|
|
861
|
+
/\[\[tool\.poetry\.source\]\][\s\S]*?(?=(\n\[\[|\n\[|$))/,
|
|
862
|
+
block.replace(/\n$/, "")
|
|
863
|
+
);
|
|
864
|
+
} else {
|
|
865
|
+
body = (body.replace(/\n*$/, "") + (body ? "\n\n" : "") + block).replace(/^\n+/, "");
|
|
866
|
+
}
|
|
867
|
+
body = body.replace(/\n*$/, "\n");
|
|
868
|
+
return writeConfig("poetry", file, registry, body);
|
|
869
|
+
}
|
|
870
|
+
function configureUv(root, proxy) {
|
|
871
|
+
const file = join3(root, "uv.toml");
|
|
872
|
+
const registry = pypiSimpleIndex(proxy);
|
|
873
|
+
const block = [
|
|
874
|
+
"[[index]]",
|
|
875
|
+
'name = "inner"',
|
|
876
|
+
`url = "${registry}"`,
|
|
877
|
+
"default = true",
|
|
878
|
+
""
|
|
879
|
+
].join("\n");
|
|
880
|
+
let body = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
881
|
+
if (/\[\[index\]\][\s\S]*?name\s*=\s*"inner"/.test(body)) {
|
|
882
|
+
body = body.replace(/\[\[index\]\][\s\S]*?(?=(\n\[\[|\n\[|$))/, block.replace(/\n$/, ""));
|
|
883
|
+
} else {
|
|
884
|
+
body = (body.replace(/\n*$/, "") + (body ? "\n\n" : "") + block).replace(/^\n+/, "");
|
|
885
|
+
}
|
|
886
|
+
body = body.replace(/\n*$/, "\n");
|
|
887
|
+
return writeConfig("uv", file, registry, body);
|
|
888
|
+
}
|
|
889
|
+
var CONFIGURERS = {
|
|
890
|
+
npm: configureNpm,
|
|
891
|
+
pnpm: configurePnpm,
|
|
892
|
+
yarn: configureYarn,
|
|
893
|
+
pip: configurePip,
|
|
894
|
+
poetry: configurePoetry,
|
|
895
|
+
uv: configureUv
|
|
896
|
+
};
|
|
897
|
+
function init(opts = {}) {
|
|
898
|
+
const root = opts.root ?? process.cwd();
|
|
899
|
+
const fromOpt = opts.proxyUrl?.trim();
|
|
900
|
+
const fromEnv = process.env.INNER_PROXY_URL?.trim();
|
|
901
|
+
const DEFAULT_PROXY = "https://proxy.inner.dev";
|
|
902
|
+
const proxyUrl = (fromOpt || fromEnv || DEFAULT_PROXY).replace(/\/+$/, "");
|
|
903
|
+
const proxyUrlIsDefault = !fromOpt && !fromEnv;
|
|
904
|
+
const managers = opts.managers ?? ALL_PACKAGE_MANAGERS;
|
|
905
|
+
const configured = [];
|
|
906
|
+
for (const m of managers) {
|
|
907
|
+
configured.push(CONFIGURERS[m](root, proxyUrl));
|
|
908
|
+
}
|
|
909
|
+
return { configured, proxyUrl, proxyUrlIsDefault };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/inner-cli.ts
|
|
913
|
+
var HELP = `inner \u2014 Inner security CLI
|
|
914
|
+
|
|
915
|
+
Usage:
|
|
916
|
+
inner init [options] Configure npm/pnpm/yarn/pip/poetry/uv to use the Inner proxy
|
|
917
|
+
inner scan [options] Zero-auth instant dependency scan (consumes Verdict API / C1)
|
|
918
|
+
|
|
919
|
+
init options:
|
|
920
|
+
--dir <path> Project root to write config into (default: cwd)
|
|
921
|
+
--proxy <url> Registry proxy base URL (default: $INNER_PROXY_URL or placeholder)
|
|
922
|
+
--managers <list> Comma-separated subset of: ${ALL_PACKAGE_MANAGERS.join(",")}
|
|
923
|
+
|
|
924
|
+
scan options:
|
|
925
|
+
--dir <path> Project root to scan (default: cwd)
|
|
926
|
+
--api-url <url> Verdict API base URL (default: $INNER_API_URL or local mock)
|
|
927
|
+
--json Machine-readable JSON output
|
|
928
|
+
--no-cache Skip the fail-open .inner-cache/
|
|
929
|
+
|
|
930
|
+
-h, --help Show this help
|
|
931
|
+
`;
|
|
932
|
+
function parseManagers(raw) {
|
|
933
|
+
if (!raw) return void 0;
|
|
934
|
+
const wanted = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
935
|
+
const valid = wanted.filter(
|
|
936
|
+
(m) => ALL_PACKAGE_MANAGERS.includes(m)
|
|
937
|
+
);
|
|
938
|
+
return valid.length ? valid : void 0;
|
|
939
|
+
}
|
|
940
|
+
async function runInit(flags) {
|
|
941
|
+
const dir = flagString(flags, "dir");
|
|
942
|
+
const proxy = flagString(flags, "proxy");
|
|
943
|
+
const managers = parseManagers(flagString(flags, "managers"));
|
|
944
|
+
const result = init({ root: dir, proxyUrl: proxy, managers });
|
|
945
|
+
process.stdout.write(`inner init \u2014 configured ${result.configured.length} package manager(s)
|
|
946
|
+
`);
|
|
947
|
+
process.stdout.write(` proxy: ${result.proxyUrl}
|
|
948
|
+
`);
|
|
949
|
+
for (const c of result.configured) {
|
|
950
|
+
process.stdout.write(` \u2713 ${c.manager.padEnd(7)} ${c.outcome.padEnd(7)} ${c.file}
|
|
951
|
+
`);
|
|
952
|
+
process.stdout.write(` registry: ${c.registry}
|
|
953
|
+
`);
|
|
954
|
+
}
|
|
955
|
+
if (result.proxyUrlIsDefault) {
|
|
956
|
+
process.stdout.write(
|
|
957
|
+
` note: using placeholder proxy ${result.proxyUrl} \u2014 set INNER_PROXY_URL or --proxy to the production proxy host.
|
|
958
|
+
`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
return 0;
|
|
962
|
+
}
|
|
963
|
+
async function runScan(flags) {
|
|
964
|
+
return runScanCommand({
|
|
965
|
+
root: flagString(flags, "dir"),
|
|
966
|
+
apiUrl: flagString(flags, "api-url"),
|
|
967
|
+
noCache: flagBool(flags, "no-cache"),
|
|
968
|
+
json: flagBool(flags, "json")
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
async function main() {
|
|
972
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
973
|
+
const [command] = parsed._;
|
|
974
|
+
if (!command || flagBool(parsed.flags, "help")) {
|
|
975
|
+
process.stdout.write(HELP);
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
switch (command) {
|
|
979
|
+
case "init":
|
|
980
|
+
return runInit(parsed.flags);
|
|
981
|
+
case "scan":
|
|
982
|
+
return runScan(parsed.flags);
|
|
983
|
+
default:
|
|
984
|
+
process.stderr.write(`inner: unknown command "${command}"
|
|
985
|
+
|
|
986
|
+
`);
|
|
987
|
+
process.stdout.write(HELP);
|
|
988
|
+
return 2;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
992
|
+
process.stderr.write(`inner: ${err instanceof Error ? err.message : String(err)}
|
|
993
|
+
`);
|
|
994
|
+
process.exit(2);
|
|
995
|
+
});
|