@zivis/mcp 0.1.10 → 0.1.12
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/dist/http-probe/capture.d.ts +51 -0
- package/dist/http-probe/capture.js +96 -0
- package/dist/http-probe/compare.d.ts +43 -0
- package/dist/http-probe/compare.js +117 -0
- package/dist/http-probe/index.d.ts +2 -0
- package/dist/http-probe/index.js +2 -0
- package/dist/pattern-packs/zivis-public-0.2.0/manifest.json +1 -1
- package/dist/redact.d.ts +30 -0
- package/dist/redact.js +164 -0
- package/dist/scanners/index.d.ts +3 -0
- package/dist/scanners/index.js +2 -0
- package/dist/scanners/normalize.d.ts +6 -0
- package/dist/scanners/normalize.js +181 -0
- package/dist/scanners/run.d.ts +27 -0
- package/dist/scanners/run.js +142 -0
- package/dist/scanners/types.d.ts +40 -0
- package/dist/scanners/types.js +1 -0
- package/dist/server.js +0 -5
- package/dist/tools/create-diagram.d.ts +3 -73
- package/dist/tools/create-diagram.js +8 -100
- package/dist/tools/get-diagram.d.ts +1 -1
- package/dist/tools/get-diagram.js +2 -52
- package/dist/tools/list-diagrams.d.ts +1 -1
- package/dist/tools/list-diagrams.js +2 -3
- package/dist/tools/manage-diagram.d.ts +1 -64
- package/dist/tools/manage-diagram.js +2 -211
- package/dist/tools/update-mermaid-source.d.ts +1 -1
- package/dist/tools/update-mermaid-source.js +1 -3
- package/package.json +5 -2
- package/dist/tools/generate-diagram.d.ts +0 -30
- package/dist/tools/generate-diagram.js +0 -161
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface CaptureRequestSpec {
|
|
2
|
+
url: string;
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
body?: string | Record<string, unknown> | unknown[];
|
|
6
|
+
label?: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CapturedRequest {
|
|
10
|
+
method: string;
|
|
11
|
+
url: string;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
body?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CapturedResponse {
|
|
17
|
+
status: number;
|
|
18
|
+
status_text?: string;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
body?: string;
|
|
21
|
+
body_json?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface HttpCapture {
|
|
24
|
+
request: CapturedRequest;
|
|
25
|
+
response: CapturedResponse;
|
|
26
|
+
meta: {
|
|
27
|
+
captured_at: string;
|
|
28
|
+
duration_ms: number;
|
|
29
|
+
request_fingerprint: string;
|
|
30
|
+
redacted: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export type FetchLike = (url: string, init: {
|
|
35
|
+
method: string;
|
|
36
|
+
headers: Record<string, string>;
|
|
37
|
+
body?: string;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}) => Promise<{
|
|
40
|
+
status: number;
|
|
41
|
+
statusText?: string;
|
|
42
|
+
headers: {
|
|
43
|
+
forEach(cb: (value: string, key: string) => void): void;
|
|
44
|
+
};
|
|
45
|
+
text(): Promise<string>;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function requestFingerprint(req: CapturedRequest): string;
|
|
48
|
+
export declare function captureRequest(spec: CaptureRequestSpec, deps?: {
|
|
49
|
+
fetchImpl?: FetchLike;
|
|
50
|
+
}): Promise<HttpCapture>;
|
|
51
|
+
export declare function redactCapture(capture: HttpCapture): HttpCapture;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import { redactValue } from "../redact.js";
|
|
3
|
+
function normalizeHeaders(headers) {
|
|
4
|
+
const out = {};
|
|
5
|
+
for (const [k, v] of Object.entries(headers ?? {}))
|
|
6
|
+
out[k.toLowerCase()] = String(v);
|
|
7
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
8
|
+
}
|
|
9
|
+
function bodyToString(body) {
|
|
10
|
+
if (body === undefined || body === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (typeof body === "string")
|
|
13
|
+
return body;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(body);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return String(body);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function requestFingerprint(req) {
|
|
22
|
+
const headerLines = Object.entries(req.headers)
|
|
23
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
const canonical = `${req.method} ${req.url}\n${headerLines}\n\n${req.body ?? ""}`;
|
|
26
|
+
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
27
|
+
}
|
|
28
|
+
function tryJson(text) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function captureRequest(spec, deps = {}) {
|
|
37
|
+
const method = (spec.method ?? "GET").toUpperCase();
|
|
38
|
+
const headers = normalizeHeaders(spec.headers);
|
|
39
|
+
const body = bodyToString(spec.body);
|
|
40
|
+
const request = { method, url: spec.url, headers, body, label: spec.label };
|
|
41
|
+
const fingerprint = requestFingerprint(request);
|
|
42
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(), spec.timeoutMs ?? 30_000);
|
|
45
|
+
const started = Date.now();
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetchImpl(spec.url, { method, headers, body, signal: controller.signal });
|
|
48
|
+
const respHeaders = {};
|
|
49
|
+
res.headers.forEach((value, key) => {
|
|
50
|
+
respHeaders[key.toLowerCase()] = value;
|
|
51
|
+
});
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
const bodyJson = tryJson(text);
|
|
54
|
+
return {
|
|
55
|
+
request,
|
|
56
|
+
response: {
|
|
57
|
+
status: res.status,
|
|
58
|
+
status_text: res.statusText,
|
|
59
|
+
headers: Object.fromEntries(Object.entries(respHeaders).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
|
|
60
|
+
body: text,
|
|
61
|
+
body_json: bodyJson,
|
|
62
|
+
},
|
|
63
|
+
meta: {
|
|
64
|
+
captured_at: new Date().toISOString(),
|
|
65
|
+
duration_ms: Date.now() - started,
|
|
66
|
+
request_fingerprint: fingerprint,
|
|
67
|
+
redacted: false,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
request,
|
|
74
|
+
response: { status: 0, headers: {} },
|
|
75
|
+
meta: {
|
|
76
|
+
captured_at: new Date().toISOString(),
|
|
77
|
+
duration_ms: Date.now() - started,
|
|
78
|
+
request_fingerprint: fingerprint,
|
|
79
|
+
redacted: false,
|
|
80
|
+
error: err instanceof Error ? err.message : String(err),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function redactCapture(capture) {
|
|
89
|
+
const request = redactValue(capture.request).value;
|
|
90
|
+
const response = redactValue(capture.response).value;
|
|
91
|
+
return {
|
|
92
|
+
request,
|
|
93
|
+
response,
|
|
94
|
+
meta: { ...capture.meta, redacted: true },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HttpCapture } from "./capture.js";
|
|
2
|
+
export interface StatusComparison {
|
|
3
|
+
a: number;
|
|
4
|
+
b: number;
|
|
5
|
+
changed: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface JsonPathDiff {
|
|
8
|
+
path: string;
|
|
9
|
+
kind: "added" | "removed" | "changed";
|
|
10
|
+
a?: unknown;
|
|
11
|
+
b?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface BodyComparison {
|
|
14
|
+
equal: boolean;
|
|
15
|
+
json_diffs?: JsonPathDiff[];
|
|
16
|
+
opaque_change?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface HeaderDiff {
|
|
19
|
+
header: string;
|
|
20
|
+
kind: "added" | "removed" | "changed";
|
|
21
|
+
a?: string;
|
|
22
|
+
b?: string;
|
|
23
|
+
}
|
|
24
|
+
export type AccessSignal = "same_status" | "both_success" | "both_denied" | "access_differential" | "one_error" | "mixed";
|
|
25
|
+
export interface CaptureComparison {
|
|
26
|
+
equal: boolean;
|
|
27
|
+
labels: {
|
|
28
|
+
a?: string;
|
|
29
|
+
b?: string;
|
|
30
|
+
};
|
|
31
|
+
status: StatusComparison;
|
|
32
|
+
headers: HeaderDiff[];
|
|
33
|
+
body: BodyComparison;
|
|
34
|
+
access_signal: AccessSignal;
|
|
35
|
+
summary: string;
|
|
36
|
+
same_request: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface CompareOptions {
|
|
39
|
+
headerAllowlist?: string[];
|
|
40
|
+
allHeaders?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function diffJson(a: unknown, b: unknown, basePath?: string): JsonPathDiff[];
|
|
43
|
+
export declare function compareCaptures(a: HttpCapture, b: HttpCapture, opts?: CompareOptions): CaptureComparison;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const VOLATILE_HEADERS = new Set([
|
|
2
|
+
"date",
|
|
3
|
+
"age",
|
|
4
|
+
"etag",
|
|
5
|
+
"last-modified",
|
|
6
|
+
"expires",
|
|
7
|
+
"set-cookie",
|
|
8
|
+
"x-request-id",
|
|
9
|
+
"x-correlation-id",
|
|
10
|
+
"cf-ray",
|
|
11
|
+
"x-amz-request-id",
|
|
12
|
+
"x-amz-id-2",
|
|
13
|
+
"x-served-by",
|
|
14
|
+
"x-timer",
|
|
15
|
+
"x-runtime",
|
|
16
|
+
"server-timing",
|
|
17
|
+
"content-length",
|
|
18
|
+
]);
|
|
19
|
+
function isDenied(status) {
|
|
20
|
+
return status === 401 || status === 403;
|
|
21
|
+
}
|
|
22
|
+
function isSuccess(status) {
|
|
23
|
+
return status >= 200 && status < 300;
|
|
24
|
+
}
|
|
25
|
+
function isError(status) {
|
|
26
|
+
return status === 0 || status >= 500;
|
|
27
|
+
}
|
|
28
|
+
function classifyAccess(a, b) {
|
|
29
|
+
if (isError(a) || isError(b))
|
|
30
|
+
return "one_error";
|
|
31
|
+
if (isSuccess(a) && isSuccess(b))
|
|
32
|
+
return "both_success";
|
|
33
|
+
if (isDenied(a) && isDenied(b))
|
|
34
|
+
return "both_denied";
|
|
35
|
+
if ((isSuccess(a) && isDenied(b)) || (isDenied(a) && isSuccess(b)))
|
|
36
|
+
return "access_differential";
|
|
37
|
+
if (a === b)
|
|
38
|
+
return "same_status";
|
|
39
|
+
return "mixed";
|
|
40
|
+
}
|
|
41
|
+
function diffHeaders(a, b, opts) {
|
|
42
|
+
const allow = opts.headerAllowlist ? new Set(opts.headerAllowlist.map((h) => h.toLowerCase())) : null;
|
|
43
|
+
const keep = (h) => (allow ? allow.has(h) : opts.allHeaders ? true : !VOLATILE_HEADERS.has(h));
|
|
44
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)].filter(keep));
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const h of [...keys].sort()) {
|
|
47
|
+
const av = a[h];
|
|
48
|
+
const bv = b[h];
|
|
49
|
+
if (av === bv)
|
|
50
|
+
continue;
|
|
51
|
+
if (av === undefined)
|
|
52
|
+
out.push({ header: h, kind: "added", b: bv });
|
|
53
|
+
else if (bv === undefined)
|
|
54
|
+
out.push({ header: h, kind: "removed", a: av });
|
|
55
|
+
else
|
|
56
|
+
out.push({ header: h, kind: "changed", a: av, b: bv });
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
export function diffJson(a, b, basePath = "$") {
|
|
61
|
+
const diffs = [];
|
|
62
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
63
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
64
|
+
const max = Math.max(a.length, b.length);
|
|
65
|
+
for (let i = 0; i < max; i++) {
|
|
66
|
+
const p = `${basePath}[${i}]`;
|
|
67
|
+
if (i >= a.length)
|
|
68
|
+
diffs.push({ path: p, kind: "added", b: b[i] });
|
|
69
|
+
else if (i >= b.length)
|
|
70
|
+
diffs.push({ path: p, kind: "removed", a: a[i] });
|
|
71
|
+
else
|
|
72
|
+
diffs.push(...diffJson(a[i], b[i], p));
|
|
73
|
+
}
|
|
74
|
+
return diffs;
|
|
75
|
+
}
|
|
76
|
+
if (isObj(a) && isObj(b)) {
|
|
77
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
78
|
+
for (const k of [...keys].sort()) {
|
|
79
|
+
const p = `${basePath}.${k}`;
|
|
80
|
+
if (!(k in a))
|
|
81
|
+
diffs.push({ path: p, kind: "added", b: b[k] });
|
|
82
|
+
else if (!(k in b))
|
|
83
|
+
diffs.push({ path: p, kind: "removed", a: a[k] });
|
|
84
|
+
else
|
|
85
|
+
diffs.push(...diffJson(a[k], b[k], p));
|
|
86
|
+
}
|
|
87
|
+
return diffs;
|
|
88
|
+
}
|
|
89
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
90
|
+
diffs.push({ path: basePath, kind: "changed", a, b });
|
|
91
|
+
}
|
|
92
|
+
return diffs;
|
|
93
|
+
}
|
|
94
|
+
function compareBodies(a, b) {
|
|
95
|
+
const aHasJson = a.body_json !== undefined;
|
|
96
|
+
const bHasJson = b.body_json !== undefined;
|
|
97
|
+
if (aHasJson && bHasJson) {
|
|
98
|
+
const json_diffs = diffJson(a.body_json, b.body_json);
|
|
99
|
+
return { equal: json_diffs.length === 0, json_diffs };
|
|
100
|
+
}
|
|
101
|
+
const equal = (a.body ?? "") === (b.body ?? "");
|
|
102
|
+
return { equal, opaque_change: !equal };
|
|
103
|
+
}
|
|
104
|
+
export function compareCaptures(a, b, opts = {}) {
|
|
105
|
+
const status = { a: a.response.status, b: b.response.status, changed: a.response.status !== b.response.status };
|
|
106
|
+
const headers = diffHeaders(a.response.headers, b.response.headers, opts);
|
|
107
|
+
const body = compareBodies(a.response, b.response);
|
|
108
|
+
const access_signal = classifyAccess(a.response.status, b.response.status);
|
|
109
|
+
const same_request = a.meta.request_fingerprint === b.meta.request_fingerprint;
|
|
110
|
+
const equal = !status.changed && headers.length === 0 && body.equal;
|
|
111
|
+
const la = a.request.label ?? "A";
|
|
112
|
+
const lb = b.request.label ?? "B";
|
|
113
|
+
const summary = equal
|
|
114
|
+
? `${la} and ${lb} are equivalent (status ${status.a}, no header/body differences).`
|
|
115
|
+
: `${la}=${status.a} vs ${lb}=${status.b}; ${headers.length} header diff(s), body ${body.equal ? "identical" : "differs"}; access_signal=${access_signal}.`;
|
|
116
|
+
return { equal, labels: { a: a.request.label, b: b.request.label }, status, headers, body, access_signal, summary, same_request };
|
|
117
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { captureRequest, redactCapture, requestFingerprint, type CaptureRequestSpec, type CapturedRequest, type CapturedResponse, type HttpCapture, type FetchLike, } from "./capture.js";
|
|
2
|
+
export { compareCaptures, diffJson, type CaptureComparison, type CompareOptions, type StatusComparison, type BodyComparison, type HeaderDiff, type JsonPathDiff, type AccessSignal, } from "./compare.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"pack_id": "zivis-public",
|
|
4
4
|
"pack_name": "ZIVIS Public Pattern Pack",
|
|
5
5
|
"version": "0.2.0",
|
|
6
|
-
"built_at": "2026-
|
|
6
|
+
"built_at": "2026-09-01T21:19:00.226Z",
|
|
7
7
|
"tier": "customer_safe",
|
|
8
8
|
"description": "ZIVIS-curated public pattern pack — capsules + inference prompts evaluated locally on the user's machine.",
|
|
9
9
|
"capsules": [
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface RedactionFinding {
|
|
2
|
+
rule: string;
|
|
3
|
+
path: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RedactionReport {
|
|
6
|
+
redacted: boolean;
|
|
7
|
+
count: number;
|
|
8
|
+
byRule: Record<string, number>;
|
|
9
|
+
findings: RedactionFinding[];
|
|
10
|
+
}
|
|
11
|
+
interface RedactRule {
|
|
12
|
+
id: string;
|
|
13
|
+
regex: RegExp;
|
|
14
|
+
secretGroup?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const REDACTION_RULES: readonly RedactRule[];
|
|
17
|
+
export declare function redactString(input: string, report?: RedactionReport, path?: string): {
|
|
18
|
+
text: string;
|
|
19
|
+
report: RedactionReport;
|
|
20
|
+
};
|
|
21
|
+
export declare const SENSITIVE_KEY_NAMES: ReadonlySet<string>;
|
|
22
|
+
export interface RedactOptions {
|
|
23
|
+
maxDepth?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function redactValue<T>(value: T, opts?: RedactOptions): {
|
|
26
|
+
value: T;
|
|
27
|
+
report: RedactionReport;
|
|
28
|
+
};
|
|
29
|
+
export declare function redactEvidence<T>(value: T): T;
|
|
30
|
+
export {};
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
function emptyReport() {
|
|
2
|
+
return { redacted: false, count: 0, byRule: {}, findings: [] };
|
|
3
|
+
}
|
|
4
|
+
function record(report, rule, path) {
|
|
5
|
+
report.redacted = true;
|
|
6
|
+
report.count += 1;
|
|
7
|
+
report.byRule[rule] = (report.byRule[rule] ?? 0) + 1;
|
|
8
|
+
report.findings.push({ rule, path });
|
|
9
|
+
}
|
|
10
|
+
const MASK = (id) => `[REDACTED:${id}]`;
|
|
11
|
+
export const REDACTION_RULES = [
|
|
12
|
+
{
|
|
13
|
+
id: "auth-header",
|
|
14
|
+
regex: /^([ \t]*(?:authorization|proxy-authorization|www-authenticate|cookie|set-cookie|x-api-key|x-apikey|x-auth-token|x-access-token|x-session-token|x-refresh-token|x-amz-security-token|api-key|apikey)[ \t]*:[ \t]*)(.+)$/gim,
|
|
15
|
+
secretGroup: 2,
|
|
16
|
+
},
|
|
17
|
+
{ id: "bearer-token", regex: /\b(Bearer[ \t]+)([A-Za-z0-9._~+/=-]{8,})/gi, secretGroup: 2 },
|
|
18
|
+
{ id: "basic-auth", regex: /\b(Basic[ \t]+)([A-Za-z0-9+/=]{8,})/gi, secretGroup: 2 },
|
|
19
|
+
{
|
|
20
|
+
id: "private-key",
|
|
21
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "private-key",
|
|
25
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g,
|
|
26
|
+
},
|
|
27
|
+
{ id: "jwt", regex: /\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g },
|
|
28
|
+
{ id: "aws-access-key-id", regex: /\b(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g },
|
|
29
|
+
{
|
|
30
|
+
id: "aws-secret-access-key",
|
|
31
|
+
regex: /(aws_?secret_?access_?key["' ]*[=:]["' ]*)([A-Za-z0-9/+=]{40})/gi,
|
|
32
|
+
secretGroup: 2,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "azure-storage-key",
|
|
36
|
+
regex: /(AccountKey=)([A-Za-z0-9+/=]{40,})/gi,
|
|
37
|
+
secretGroup: 2,
|
|
38
|
+
},
|
|
39
|
+
{ id: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
40
|
+
{ id: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9_]{36,}\b/g },
|
|
41
|
+
{ id: "slack-token", regex: /\bxox[baprs]-[0-9A-Za-z-]{10,}/g },
|
|
42
|
+
{ id: "slack-webhook", regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g },
|
|
43
|
+
{ id: "stripe-key", regex: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
|
|
44
|
+
{ id: "sendgrid-key", regex: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g },
|
|
45
|
+
{ id: "npm-token", regex: /\bnpm_[A-Za-z0-9]{36}\b/g },
|
|
46
|
+
{ id: "anthropic-key", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
47
|
+
{ id: "openai-key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
48
|
+
{
|
|
49
|
+
id: "connection-string-password",
|
|
50
|
+
regex: /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|rediss|amqps?|https?):\/\/[^:@/\s]+:)([^@/\s]+)(@)/gi,
|
|
51
|
+
secretGroup: 2,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "generic-secret-assignment",
|
|
55
|
+
regex: /\b(password|passwd|pwd|secret|client[_-]?secret|api[_-]?key|apikey|access[_-]?token|refresh[_-]?token|auth[_-]?token|session[_-]?token|private[_-]?key|passphrase)\b(["' ]*[=:]["' ]*)([^\s"',;&})]{6,})/gi,
|
|
56
|
+
secretGroup: 3,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
export function redactString(input, report = emptyReport(), path = "") {
|
|
60
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
61
|
+
return { text: input, report };
|
|
62
|
+
}
|
|
63
|
+
let text = input;
|
|
64
|
+
for (const rule of REDACTION_RULES) {
|
|
65
|
+
if (rule.secretGroup === undefined) {
|
|
66
|
+
text = text.replace(rule.regex, () => {
|
|
67
|
+
record(report, rule.id, path);
|
|
68
|
+
return MASK(rule.id);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const g = rule.secretGroup;
|
|
73
|
+
text = text.replace(rule.regex, (match, ...groups) => {
|
|
74
|
+
const secret = groups[g - 1];
|
|
75
|
+
if (secret === undefined || secret === "")
|
|
76
|
+
return match;
|
|
77
|
+
record(report, rule.id, path);
|
|
78
|
+
const idx = match.lastIndexOf(String(secret));
|
|
79
|
+
if (idx < 0)
|
|
80
|
+
return MASK(rule.id);
|
|
81
|
+
return match.slice(0, idx) + MASK(rule.id) + match.slice(idx + String(secret).length);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { text, report };
|
|
86
|
+
}
|
|
87
|
+
export const SENSITIVE_KEY_NAMES = new Set([
|
|
88
|
+
"authorization",
|
|
89
|
+
"proxyauthorization",
|
|
90
|
+
"cookie",
|
|
91
|
+
"setcookie",
|
|
92
|
+
"xapikey",
|
|
93
|
+
"xauthtoken",
|
|
94
|
+
"xaccesstoken",
|
|
95
|
+
"xsessiontoken",
|
|
96
|
+
"xrefreshtoken",
|
|
97
|
+
"xamzsecuritytoken",
|
|
98
|
+
"apikey",
|
|
99
|
+
"apitoken",
|
|
100
|
+
"password",
|
|
101
|
+
"passwd",
|
|
102
|
+
"pwd",
|
|
103
|
+
"passphrase",
|
|
104
|
+
"secret",
|
|
105
|
+
"clientsecret",
|
|
106
|
+
"secretkey",
|
|
107
|
+
"token",
|
|
108
|
+
"accesstoken",
|
|
109
|
+
"refreshtoken",
|
|
110
|
+
"idtoken",
|
|
111
|
+
"authtoken",
|
|
112
|
+
"sessiontoken",
|
|
113
|
+
"sessionid",
|
|
114
|
+
"session",
|
|
115
|
+
"privatekey",
|
|
116
|
+
"credential",
|
|
117
|
+
"credentials",
|
|
118
|
+
"bearer",
|
|
119
|
+
"auth",
|
|
120
|
+
].map((k) => k.replace(/[-_]/g, "")));
|
|
121
|
+
function normalizeKey(key) {
|
|
122
|
+
return key.toLowerCase().replace(/[-_]/g, "");
|
|
123
|
+
}
|
|
124
|
+
export function redactValue(value, opts = {}) {
|
|
125
|
+
const report = emptyReport();
|
|
126
|
+
const maxDepth = opts.maxDepth ?? 64;
|
|
127
|
+
const seen = new WeakSet();
|
|
128
|
+
const walk = (node, path, depth, underSensitiveKey) => {
|
|
129
|
+
if (node === null || node === undefined)
|
|
130
|
+
return node;
|
|
131
|
+
if (typeof node === "string") {
|
|
132
|
+
if (underSensitiveKey) {
|
|
133
|
+
if (node.length > 0) {
|
|
134
|
+
record(report, "sensitive-key", path);
|
|
135
|
+
return MASK("sensitive-key");
|
|
136
|
+
}
|
|
137
|
+
return node;
|
|
138
|
+
}
|
|
139
|
+
return redactString(node, report, path).text;
|
|
140
|
+
}
|
|
141
|
+
if (typeof node !== "object")
|
|
142
|
+
return node;
|
|
143
|
+
if (seen.has(node))
|
|
144
|
+
return "[CYCLE]";
|
|
145
|
+
if (depth >= maxDepth)
|
|
146
|
+
return node;
|
|
147
|
+
seen.add(node);
|
|
148
|
+
if (Array.isArray(node)) {
|
|
149
|
+
return node.map((item, i) => walk(item, `${path}[${i}]`, depth + 1, underSensitiveKey));
|
|
150
|
+
}
|
|
151
|
+
const out = {};
|
|
152
|
+
for (const [key, v] of Object.entries(node)) {
|
|
153
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
154
|
+
const sensitive = underSensitiveKey || SENSITIVE_KEY_NAMES.has(normalizeKey(key));
|
|
155
|
+
out[key] = walk(v, childPath, depth + 1, sensitive);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
};
|
|
159
|
+
const redacted = walk(value, "", 0, false);
|
|
160
|
+
return { value: redacted, report };
|
|
161
|
+
}
|
|
162
|
+
export function redactEvidence(value) {
|
|
163
|
+
return redactValue(value).value;
|
|
164
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { ScannerId, LeadSeverity, LeadLocation, NormalizedLead, ScannerRunResult, LocalScanReport, } from "./types.js";
|
|
2
|
+
export { normalizeScannerOutput, normalizeGitleaks, normalizeTrivy, normalizeSemgrep, normalizeOsvScanner, } from "./normalize.js";
|
|
3
|
+
export { SUPPORTED_SCANNERS, SCANNER_BINARIES, scannerCommand, runOneScanner, runLocalScanners, aggregate, defaultExecEnv, type ScannerCommand, type ScannerExecEnv, type RunLocalScannersOptions, } from "./run.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NormalizedLead, ScannerId } from "./types.js";
|
|
2
|
+
export declare function normalizeGitleaks(parsed: unknown): NormalizedLead[];
|
|
3
|
+
export declare function normalizeTrivy(parsed: unknown): NormalizedLead[];
|
|
4
|
+
export declare function normalizeSemgrep(parsed: unknown): NormalizedLead[];
|
|
5
|
+
export declare function normalizeOsvScanner(parsed: unknown): NormalizedLead[];
|
|
6
|
+
export declare function normalizeScannerOutput(source: ScannerId, parsed: unknown): NormalizedLead[];
|