@riocrypto/common-server 1.0.2935 → 1.0.2937
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/build/bot/gcp-logging-client.d.ts +26 -0
- package/build/bot/gcp-logging-client.js +127 -0
- package/build/bot/git-repo-cache.d.ts +33 -0
- package/build/bot/git-repo-cache.js +244 -0
- package/build/bot/github-app-auth.d.ts +15 -0
- package/build/bot/github-app-auth.js +93 -0
- package/build/bot/injection-markers.d.ts +1 -0
- package/build/bot/injection-markers.js +16 -0
- package/build/bot/k8s-client.d.ts +45 -0
- package/build/bot/k8s-client.js +151 -0
- package/build/bot/redactor.d.ts +11 -0
- package/build/bot/redactor.js +186 -0
- package/build/bot/slack-notifier.d.ts +15 -0
- package/build/bot/slack-notifier.js +54 -0
- package/build/bot/tool.d.ts +26 -0
- package/build/bot/tool.js +60 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +14 -0
- package/build/models/security-baseline.d.ts +13 -0
- package/build/models/security-baseline.js +23 -0
- package/build/models/security-bot-audit-event.d.ts +36 -0
- package/build/models/security-bot-audit-event.js +51 -0
- package/build/models/security-bot-controls.d.ts +23 -0
- package/build/models/security-bot-controls.js +42 -0
- package/build/models/security-finding.d.ts +38 -0
- package/build/models/security-finding.js +59 -0
- package/build/models/security-known-pattern.d.ts +12 -0
- package/build/models/security-known-pattern.js +28 -0
- package/build/models/security-sweep-run.d.ts +41 -0
- package/build/models/security-sweep-run.js +67 -0
- package/build/services/logger.js +5 -0
- package/package.json +3 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Minimal, GET-only client for the in-cluster Kubernetes API.
|
|
3
|
+
//
|
|
4
|
+
// Deliberately not the official @kubernetes/client-node package. That
|
|
5
|
+
// library is ESM only, pulls in websockets, tar, socks and OIDC, and exposes
|
|
6
|
+
// exec/attach/delete on the same object we would hand to the tool layer. We
|
|
7
|
+
// need four GET endpoints. Fewer moving parts, no write surface in the code
|
|
8
|
+
// at all, and the RBAC Role bound to the pod is the second lock.
|
|
9
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
10
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
11
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
12
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
13
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
14
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
15
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.K8sClient = void 0;
|
|
23
|
+
const axios_1 = __importDefault(require("axios"));
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const https_1 = __importDefault(require("https"));
|
|
26
|
+
const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
|
|
27
|
+
const minutesSince = (iso) => {
|
|
28
|
+
if (!iso)
|
|
29
|
+
return -1;
|
|
30
|
+
const t = new Date(iso).getTime();
|
|
31
|
+
return Number.isNaN(t) ? -1 : Math.floor((Date.now() - t) / 60000);
|
|
32
|
+
};
|
|
33
|
+
class K8sClient {
|
|
34
|
+
constructor(baseURL, ca, timeoutMs) {
|
|
35
|
+
this.http = axios_1.default.create({
|
|
36
|
+
baseURL,
|
|
37
|
+
timeout: timeoutMs,
|
|
38
|
+
httpsAgent: ca ? new https_1.default.Agent({ ca }) : undefined,
|
|
39
|
+
// Only GET is ever issued. Kept explicit so a future refactor cannot
|
|
40
|
+
// silently add a body-carrying method through this instance.
|
|
41
|
+
maxRedirects: 0,
|
|
42
|
+
validateStatus: (s) => s >= 200 && s < 300,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
static fromInCluster(timeoutMs) {
|
|
46
|
+
const host = process.env.KUBERNETES_SERVICE_HOST;
|
|
47
|
+
const port = process.env.KUBERNETES_SERVICE_PORT || "443";
|
|
48
|
+
if (!host || !fs_1.default.existsSync(`${SA_DIR}/token`)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const ca = fs_1.default.existsSync(`${SA_DIR}/ca.crt`)
|
|
52
|
+
? fs_1.default.readFileSync(`${SA_DIR}/ca.crt`)
|
|
53
|
+
: undefined;
|
|
54
|
+
return new K8sClient(`https://${host}:${port}`, ca, timeoutMs);
|
|
55
|
+
}
|
|
56
|
+
// Projected tokens rotate, so read on every call rather than caching.
|
|
57
|
+
headers() {
|
|
58
|
+
const token = fs_1.default.readFileSync(`${SA_DIR}/token`, "utf8").trim();
|
|
59
|
+
return { Authorization: `Bearer ${token}`, Accept: "application/json" };
|
|
60
|
+
}
|
|
61
|
+
get(path, params) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
const res = yield this.http.get(path, { params, headers: this.headers() });
|
|
64
|
+
return res.data;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
listPods(namespace) {
|
|
68
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
const data = yield this.get(`/api/v1/namespaces/${namespace}/pods`, {
|
|
70
|
+
limit: 500,
|
|
71
|
+
});
|
|
72
|
+
return (data.items || []).map((pod) => {
|
|
73
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
74
|
+
const statuses = ((_a = pod.status) === null || _a === void 0 ? void 0 : _a.containerStatuses) || [];
|
|
75
|
+
const readyCount = statuses.filter((s) => s.ready).length;
|
|
76
|
+
const restarts = statuses.reduce((n, s) => n + (s.restartCount || 0), 0);
|
|
77
|
+
const lastState = statuses
|
|
78
|
+
.map((s) => {
|
|
79
|
+
var _a;
|
|
80
|
+
const term = (_a = s.lastState) === null || _a === void 0 ? void 0 : _a.terminated;
|
|
81
|
+
if (!term)
|
|
82
|
+
return null;
|
|
83
|
+
return `${s.name}: ${term.reason || "terminated"} exit=${term.exitCode} at ${term.finishedAt}`;
|
|
84
|
+
})
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join("; ");
|
|
87
|
+
return Object.assign({ name: (_b = pod.metadata) === null || _b === void 0 ? void 0 : _b.name, phase: (_c = pod.status) === null || _c === void 0 ? void 0 : _c.phase, ready: `${readyCount}/${(((_d = pod.spec) === null || _d === void 0 ? void 0 : _d.containers) || []).length}`, restarts, ageMinutes: minutesSince((_e = pod.metadata) === null || _e === void 0 ? void 0 : _e.creationTimestamp), containers: (((_f = pod.spec) === null || _f === void 0 ? void 0 : _f.containers) || []).map((c) => c.name), node: (_g = pod.spec) === null || _g === void 0 ? void 0 : _g.nodeName }, (lastState ? { lastState } : {}));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
getPodLog(namespace, pod, options) {
|
|
92
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
const params = {
|
|
94
|
+
tailLines: options.tailLines,
|
|
95
|
+
sinceSeconds: options.sinceSeconds,
|
|
96
|
+
timestamps: true,
|
|
97
|
+
};
|
|
98
|
+
if (options.container)
|
|
99
|
+
params.container = options.container;
|
|
100
|
+
if (options.previous)
|
|
101
|
+
params.previous = true;
|
|
102
|
+
const res = yield this.http.get(`/api/v1/namespaces/${namespace}/pods/${pod}/log`, {
|
|
103
|
+
params,
|
|
104
|
+
headers: Object.assign(Object.assign({}, this.headers()), { Accept: "text/plain" }),
|
|
105
|
+
responseType: "text",
|
|
106
|
+
transformResponse: (d) => d,
|
|
107
|
+
});
|
|
108
|
+
return typeof res.data === "string" ? res.data : String(res.data);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
listDeployments(namespace) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
const data = yield this.get(`/apis/apps/v1/namespaces/${namespace}/deployments`, { limit: 500 });
|
|
114
|
+
return (data.items || []).map((d) => {
|
|
115
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
116
|
+
return ({
|
|
117
|
+
name: (_a = d.metadata) === null || _a === void 0 ? void 0 : _a.name,
|
|
118
|
+
desired: (_c = (_b = d.spec) === null || _b === void 0 ? void 0 : _b.replicas) !== null && _c !== void 0 ? _c : 0,
|
|
119
|
+
ready: (_e = (_d = d.status) === null || _d === void 0 ? void 0 : _d.readyReplicas) !== null && _e !== void 0 ? _e : 0,
|
|
120
|
+
available: (_g = (_f = d.status) === null || _f === void 0 ? void 0 : _f.availableReplicas) !== null && _g !== void 0 ? _g : 0,
|
|
121
|
+
updated: (_j = (_h = d.status) === null || _h === void 0 ? void 0 : _h.updatedReplicas) !== null && _j !== void 0 ? _j : 0,
|
|
122
|
+
images: (((_m = (_l = (_k = d.spec) === null || _k === void 0 ? void 0 : _k.template) === null || _l === void 0 ? void 0 : _l.spec) === null || _m === void 0 ? void 0 : _m.containers) || []).map((c) => c.image),
|
|
123
|
+
conditions: (((_o = d.status) === null || _o === void 0 ? void 0 : _o.conditions) || []).map((c) => `${c.type}=${c.status}${c.reason ? ` (${c.reason})` : ""}`),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
listEvents(namespace, involvedObjectName) {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
const params = { limit: 500 };
|
|
131
|
+
if (involvedObjectName) {
|
|
132
|
+
params.fieldSelector = `involvedObject.name=${involvedObjectName}`;
|
|
133
|
+
}
|
|
134
|
+
const data = yield this.get(`/api/v1/namespaces/${namespace}/events`, params);
|
|
135
|
+
return (data.items || [])
|
|
136
|
+
.map((e) => {
|
|
137
|
+
var _a, _b, _c;
|
|
138
|
+
return ({
|
|
139
|
+
lastSeen: e.lastTimestamp || e.eventTime || ((_a = e.metadata) === null || _a === void 0 ? void 0 : _a.creationTimestamp) || "",
|
|
140
|
+
type: e.type,
|
|
141
|
+
reason: e.reason,
|
|
142
|
+
object: `${(_b = e.involvedObject) === null || _b === void 0 ? void 0 : _b.kind}/${(_c = e.involvedObject) === null || _c === void 0 ? void 0 : _c.name}`,
|
|
143
|
+
count: e.count || 1,
|
|
144
|
+
message: e.message || "",
|
|
145
|
+
});
|
|
146
|
+
})
|
|
147
|
+
.sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : -1));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
exports.K8sClient = K8sClient;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RedactionCategory = "privateKey" | "urlCredentials" | "cookieHeader" | "credentialField" | "jwt" | "authHeader" | "apiKey" | "curp" | "rfc" | "clabe" | "phone" | "card" | "email";
|
|
2
|
+
export interface RedactionResult {
|
|
3
|
+
text: string;
|
|
4
|
+
counts: Partial<Record<RedactionCategory, number>>;
|
|
5
|
+
total: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function redact(input: string): RedactionResult;
|
|
8
|
+
export declare const CREDENTIAL_CATEGORIES: RedactionCategory[];
|
|
9
|
+
export declare function redactCredentials(input: string): RedactionResult;
|
|
10
|
+
export declare function countCredentialRedactions(counts: Partial<Record<RedactionCategory, number>>): number;
|
|
11
|
+
export declare function summarizeRedactions(counts: Partial<Record<RedactionCategory, number>>): string;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Deterministic scrubber applied to every tool result BEFORE it is placed in
|
|
3
|
+
// the model context, and again to the model's final answer before it is
|
|
4
|
+
// returned to the engineer. Nothing here depends on the model. If a value is
|
|
5
|
+
// caught by a rule it never reaches Anthropic and never reaches the browser.
|
|
6
|
+
//
|
|
7
|
+
// Rule order matters. Structured secrets (key blocks, URLs with credentials,
|
|
8
|
+
// named credential fields) run first so that a later, looser pattern does
|
|
9
|
+
// not chew up part of a match and leave a fragment behind.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.summarizeRedactions = exports.countCredentialRedactions = exports.redactCredentials = exports.CREDENTIAL_CATEGORIES = exports.redact = void 0;
|
|
12
|
+
const REDACTED = "[REDACTED]";
|
|
13
|
+
// Key names whose values are always secrets. Matched as the tail of the key
|
|
14
|
+
// so `accessToken`, `x-api-key`, `db_password` and `Authorization` all hit,
|
|
15
|
+
// while identifiers like `authId` or `tokenId` do not.
|
|
16
|
+
const CREDENTIAL_KEY = "(?:[A-Za-z0-9_.\\-]*(?:password|passwd|secret|token|api[_-]?key|apikey|authorization|cookie|private[_-]?key|mnemonic|seed[_-]?phrase|cvv|cvc|access[_-]?key|signing[_-]?key|credentials?)|seed|otp|pin|pwd|pass)";
|
|
17
|
+
const PHONE_KEY = "(?:phone|phone[_-]?number|tel|telefono|teléfono|celular|mobile|whatsapp)";
|
|
18
|
+
const keepLast4 = (digits, label) => `${label} ****${digits.slice(-4)}`;
|
|
19
|
+
const luhnValid = (digits) => {
|
|
20
|
+
let sum = 0;
|
|
21
|
+
let alternate = false;
|
|
22
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
23
|
+
let n = digits.charCodeAt(i) - 48;
|
|
24
|
+
if (alternate) {
|
|
25
|
+
n *= 2;
|
|
26
|
+
if (n > 9)
|
|
27
|
+
n -= 9;
|
|
28
|
+
}
|
|
29
|
+
sum += n;
|
|
30
|
+
alternate = !alternate;
|
|
31
|
+
}
|
|
32
|
+
return sum % 10 === 0;
|
|
33
|
+
};
|
|
34
|
+
const RULES = [
|
|
35
|
+
{
|
|
36
|
+
category: "privateKey",
|
|
37
|
+
pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
38
|
+
replace: () => "[REDACTED_PRIVATE_KEY]",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
category: "urlCredentials",
|
|
42
|
+
pattern: /(\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^\s/:@"']+):([^\s/@"']+)@/g,
|
|
43
|
+
replace: (_m, scheme, user) => `${scheme}${user}:${REDACTED}@`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
category: "cookieHeader",
|
|
47
|
+
pattern: /\b(cookie|set-cookie)(["']?\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\n"']+)/gi,
|
|
48
|
+
replace: (_m, header, sep) => `${header}${sep}${REDACTED}`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
// JSON style: "password": "hunter2" | "token": 'abc' | "otp": 123456
|
|
52
|
+
category: "credentialField",
|
|
53
|
+
pattern: new RegExp(`("${CREDENTIAL_KEY}"\\s*:\\s*)("(?:[^"\\\\]|\\\\.)*"|'[^']*'|[^,}\\]\\s]+)`, "gi"),
|
|
54
|
+
replace: (_m, prefix) => `${prefix}"${REDACTED}"`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
// Text style: password=hunter2 | Authorization: Bearer xyz | token: abc
|
|
58
|
+
category: "credentialField",
|
|
59
|
+
pattern: new RegExp(`\\b(${CREDENTIAL_KEY})(\\s*[=:]\\s*)((?:Bearer|Basic)\\s+[^\\s,;&"']+|"[^"\\n]*"|'[^'\\n]*'|[^\\s,;&"']+)`, "gi"),
|
|
60
|
+
replace: (_m, key, sep) => `${key}${sep}${REDACTED}`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
category: "jwt",
|
|
64
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
65
|
+
replace: () => "[REDACTED_JWT]",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
category: "authHeader",
|
|
69
|
+
pattern: /\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]{16,}=*/g,
|
|
70
|
+
replace: (_m, scheme) => `${scheme} ${REDACTED}`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
category: "apiKey",
|
|
74
|
+
pattern: /\b(?:sk-ant-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]{10,}|rk_live_[A-Za-z0-9]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|gh[pousr]_[A-Za-z0-9]{36,}|xox[abprs]-[A-Za-z0-9-]{10,}|glpat-[A-Za-z0-9_-]{20,})\b/g,
|
|
75
|
+
replace: () => "[REDACTED_API_KEY]",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
category: "curp",
|
|
79
|
+
pattern: /\b[A-Z]{4}\d{6}[HM][A-Z]{2}[B-DF-HJ-NP-TV-Z]{3}[A-Z0-9]\d\b/g,
|
|
80
|
+
replace: () => "[REDACTED_CURP]",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
// Field-aware only. A bare RFC shape (3-4 letters, 6 digits, 3 alnum)
|
|
84
|
+
// collides with payment tracking keys, which are exactly what this bot
|
|
85
|
+
// is used to search for.
|
|
86
|
+
category: "rfc",
|
|
87
|
+
pattern: /("?\b(?:rfc|tax[_-]?id)"?\s*[:=]\s*"?)([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})\b/gi,
|
|
88
|
+
replace: (_m, prefix) => `${prefix}[REDACTED_RFC]`,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
category: "clabe",
|
|
92
|
+
pattern: /\b\d{18}\b/g,
|
|
93
|
+
replace: (m) => keepLast4(m, "CLABE"),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
// Field-aware phone masking. A bare 10 digit run is far more likely to be
|
|
97
|
+
// a unix timestamp than a phone number, so we only mask when the number
|
|
98
|
+
// is labelled or carries an international prefix.
|
|
99
|
+
category: "phone",
|
|
100
|
+
pattern: new RegExp(`("?\\b${PHONE_KEY}"?\\s*[:=]\\s*"?)(\\+?[\\d][\\d\\s\\-()]{8,18}\\d)`, "gi"),
|
|
101
|
+
replace: (_m, prefix, number) => `${prefix}${keepLast4(number.replace(/\D/g, ""), "PHONE")}`,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
category: "phone",
|
|
105
|
+
pattern: /\+\d{1,3}[\s-]?\(?\d{2,4}\)?[\s-]?\d{3,4}[\s-]?\d{4}\b/g,
|
|
106
|
+
replace: (m) => keepLast4(m.replace(/\D/g, ""), "PHONE"),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
category: "card",
|
|
110
|
+
pattern: /\b(?:\d[ -]?){12,18}\d\b/g,
|
|
111
|
+
replace: (m) => {
|
|
112
|
+
const digits = m.replace(/\D/g, "");
|
|
113
|
+
if (digits.length < 13 || digits.length > 19 || !luhnValid(digits)) {
|
|
114
|
+
return m;
|
|
115
|
+
}
|
|
116
|
+
// 13 digit runs starting with 1 are millisecond timestamps, which
|
|
117
|
+
// pass Luhn one time in ten. Real 13 digit cards are old Visa (4...).
|
|
118
|
+
if (digits.length === 13 && digits.startsWith("1")) {
|
|
119
|
+
return m;
|
|
120
|
+
}
|
|
121
|
+
return keepLast4(digits, "CARD");
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
category: "email",
|
|
126
|
+
pattern: /\b([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*@([A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g,
|
|
127
|
+
replace: (_m, first, domain) => `${first}***@${domain}`,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
const applyRules = (input, rules) => {
|
|
131
|
+
const counts = {};
|
|
132
|
+
let text = input;
|
|
133
|
+
let total = 0;
|
|
134
|
+
for (const rule of rules) {
|
|
135
|
+
let hits = 0;
|
|
136
|
+
text = text.replace(rule.pattern, (match, ...rest) => {
|
|
137
|
+
// String.replace passes (match, ...groups, offset, string). Drop the
|
|
138
|
+
// trailing offset and source so the rule only sees its groups.
|
|
139
|
+
const groups = rest.slice(0, -2);
|
|
140
|
+
const replaced = rule.replace(match, ...groups);
|
|
141
|
+
if (replaced !== match)
|
|
142
|
+
hits++;
|
|
143
|
+
return replaced;
|
|
144
|
+
});
|
|
145
|
+
if (hits > 0) {
|
|
146
|
+
counts[rule.category] = (counts[rule.category] || 0) + hits;
|
|
147
|
+
total += hits;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { text, counts, total };
|
|
151
|
+
};
|
|
152
|
+
function redact(input) {
|
|
153
|
+
return applyRules(input, RULES);
|
|
154
|
+
}
|
|
155
|
+
exports.redact = redact;
|
|
156
|
+
// Categories that mean "a secret was present". PII categories (email, phone,
|
|
157
|
+
// CLABE, card, CURP, RFC) are masked for hygiene but are normal debugging
|
|
158
|
+
// data; only credential hits are treated as security signals.
|
|
159
|
+
exports.CREDENTIAL_CATEGORIES = [
|
|
160
|
+
"privateKey",
|
|
161
|
+
"urlCredentials",
|
|
162
|
+
"cookieHeader",
|
|
163
|
+
"credentialField",
|
|
164
|
+
"jwt",
|
|
165
|
+
"authHeader",
|
|
166
|
+
"apiKey",
|
|
167
|
+
];
|
|
168
|
+
// For the engineer's own question. Emails, ids, CLABEs and phone numbers in
|
|
169
|
+
// a question are what the engineer is asking about and must survive, so
|
|
170
|
+
// only credential material is scrubbed before the question is stored or
|
|
171
|
+
// sent to the model.
|
|
172
|
+
function redactCredentials(input) {
|
|
173
|
+
return applyRules(input, RULES.filter((rule) => exports.CREDENTIAL_CATEGORIES.includes(rule.category)));
|
|
174
|
+
}
|
|
175
|
+
exports.redactCredentials = redactCredentials;
|
|
176
|
+
function countCredentialRedactions(counts) {
|
|
177
|
+
return exports.CREDENTIAL_CATEGORIES.reduce((n, c) => n + (counts[c] || 0), 0);
|
|
178
|
+
}
|
|
179
|
+
exports.countCredentialRedactions = countCredentialRedactions;
|
|
180
|
+
function summarizeRedactions(counts) {
|
|
181
|
+
const parts = Object.entries(counts)
|
|
182
|
+
.filter(([, n]) => (n || 0) > 0)
|
|
183
|
+
.map(([category, n]) => `${category}: ${n}`);
|
|
184
|
+
return parts.length > 0 ? parts.join(", ") : "none";
|
|
185
|
+
}
|
|
186
|
+
exports.summarizeRedactions = summarizeRedactions;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SlackNotifierOptions {
|
|
2
|
+
channelId: string;
|
|
3
|
+
prefix: string;
|
|
4
|
+
localToken?: string;
|
|
5
|
+
log?: (message: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare class SlackNotifier {
|
|
8
|
+
private readonly options;
|
|
9
|
+
private queue;
|
|
10
|
+
private readonly log;
|
|
11
|
+
constructor(options: SlackNotifierOptions);
|
|
12
|
+
get enabled(): boolean;
|
|
13
|
+
notify(text: string): void;
|
|
14
|
+
private postWithLocalToken;
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SlackNotifier = void 0;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const slack_client_1 = require("../clients/slack-client");
|
|
18
|
+
// Posts are chained so they reach Slack in the order they were raised (the
|
|
19
|
+
// signal that caused a lockout before the lockout itself). Callers never
|
|
20
|
+
// wait on the chain. An empty channel id disables sending entirely.
|
|
21
|
+
class SlackNotifier {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.queue = Promise.resolve();
|
|
25
|
+
this.log = options.log || (() => undefined);
|
|
26
|
+
}
|
|
27
|
+
get enabled() {
|
|
28
|
+
return this.options.channelId.length > 0;
|
|
29
|
+
}
|
|
30
|
+
notify(text) {
|
|
31
|
+
if (!this.enabled)
|
|
32
|
+
return;
|
|
33
|
+
const prefixed = `${this.options.prefix} ${text}`;
|
|
34
|
+
this.queue = this.queue.then(() => (this.options.localToken
|
|
35
|
+
? this.postWithLocalToken(prefixed)
|
|
36
|
+
: slack_client_1.slackClient.sendMessage(prefixed, this.options.channelId))
|
|
37
|
+
.then(() => this.log(`Slack notify sent to ${this.options.channelId}`))
|
|
38
|
+
.catch((error) => this.log(`Slack notify failed: ${(error === null || error === void 0 ? void 0 : error.message) || error}`)));
|
|
39
|
+
}
|
|
40
|
+
postWithLocalToken(text) {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
const response = yield axios_1.default.post("https://slack.com/api/chat.postMessage", { channel: this.options.channelId, text }, {
|
|
44
|
+
headers: { Authorization: `Bearer ${this.options.localToken}` },
|
|
45
|
+
timeout: 10000,
|
|
46
|
+
maxRedirects: 0,
|
|
47
|
+
});
|
|
48
|
+
if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.ok)) {
|
|
49
|
+
throw new Error(((_b = response.data) === null || _b === void 0 ? void 0 : _b.error) || "unknown Slack error");
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.SlackNotifier = SlackNotifier;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class ToolInputError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare class ToolAccessError extends ToolInputError {
|
|
4
|
+
}
|
|
5
|
+
export interface ToolInputSchema {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties?: Record<string, unknown>;
|
|
8
|
+
required?: string[];
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolExecution {
|
|
12
|
+
summary: string;
|
|
13
|
+
output: string;
|
|
14
|
+
}
|
|
15
|
+
export interface BotTool {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: ToolInputSchema;
|
|
19
|
+
isSourceCode?: boolean;
|
|
20
|
+
execute(input: Record<string, unknown>): Promise<ToolExecution>;
|
|
21
|
+
}
|
|
22
|
+
export declare const requireString: (input: Record<string, unknown>, key: string, maxLength: number) => string;
|
|
23
|
+
export declare const optionalString: (input: Record<string, unknown>, key: string, maxLength: number) => string | undefined;
|
|
24
|
+
export declare const clampInt: (input: Record<string, unknown>, key: string, fallback: number, min: number, max: number) => number;
|
|
25
|
+
export declare const optionalBool: (input: Record<string, unknown>, key: string) => boolean;
|
|
26
|
+
export declare const requireK8sName: (input: Record<string, unknown>, key: string) => string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared primitives for model-facing tools. Every tool built on these is a
|
|
3
|
+
// read; the helpers exist so model input is validated the same way in every
|
|
4
|
+
// bot before it touches GCP, Kubernetes, git or Mongo.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.requireK8sName = exports.optionalBool = exports.clampInt = exports.optionalString = exports.requireString = exports.ToolAccessError = exports.ToolInputError = void 0;
|
|
7
|
+
// Raised when the model passes something a tool refuses to act on (bad type,
|
|
8
|
+
// out of range, namespace not allowed). Sent back to the model as an
|
|
9
|
+
// is_error tool_result so it can correct itself.
|
|
10
|
+
class ToolInputError extends Error {
|
|
11
|
+
}
|
|
12
|
+
exports.ToolInputError = ToolInputError;
|
|
13
|
+
// A ToolInputError that means "not allowed" rather than "malformed": a repo
|
|
14
|
+
// outside the allowlist, a denied path, a namespace out of scope. Bots count
|
|
15
|
+
// these as security signals; ordinary input mistakes are not.
|
|
16
|
+
class ToolAccessError extends ToolInputError {
|
|
17
|
+
}
|
|
18
|
+
exports.ToolAccessError = ToolAccessError;
|
|
19
|
+
const requireString = (input, key, maxLength) => {
|
|
20
|
+
const value = input[key];
|
|
21
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
22
|
+
throw new ToolInputError(`"${key}" must be a non-empty string`);
|
|
23
|
+
}
|
|
24
|
+
if (value.length > maxLength) {
|
|
25
|
+
throw new ToolInputError(`"${key}" exceeds ${maxLength} characters`);
|
|
26
|
+
}
|
|
27
|
+
return value.trim();
|
|
28
|
+
};
|
|
29
|
+
exports.requireString = requireString;
|
|
30
|
+
const optionalString = (input, key, maxLength) => {
|
|
31
|
+
if (input[key] === undefined || input[key] === null || input[key] === "") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return (0, exports.requireString)(input, key, maxLength);
|
|
35
|
+
};
|
|
36
|
+
exports.optionalString = optionalString;
|
|
37
|
+
const clampInt = (input, key, fallback, min, max) => {
|
|
38
|
+
const raw = input[key];
|
|
39
|
+
if (raw === undefined || raw === null)
|
|
40
|
+
return fallback;
|
|
41
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
42
|
+
if (!Number.isFinite(n)) {
|
|
43
|
+
throw new ToolInputError(`"${key}" must be a number`);
|
|
44
|
+
}
|
|
45
|
+
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
46
|
+
};
|
|
47
|
+
exports.clampInt = clampInt;
|
|
48
|
+
const optionalBool = (input, key) => input[key] === true || input[key] === "true";
|
|
49
|
+
exports.optionalBool = optionalBool;
|
|
50
|
+
// Kubernetes names: lowercase DNS labels. Anything else is refused before it
|
|
51
|
+
// is interpolated into an API path.
|
|
52
|
+
const K8S_NAME = /^[a-z0-9]([-a-z0-9.]{0,251}[a-z0-9])?$/;
|
|
53
|
+
const requireK8sName = (input, key) => {
|
|
54
|
+
const value = (0, exports.requireString)(input, key, 253);
|
|
55
|
+
if (!K8S_NAME.test(value)) {
|
|
56
|
+
throw new ToolInputError(`"${key}" is not a valid Kubernetes name`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
};
|
|
60
|
+
exports.requireK8sName = requireK8sName;
|
package/build/index.d.ts
CHANGED
|
@@ -138,6 +138,20 @@ export * from "./models/internal-support-chat-message";
|
|
|
138
138
|
export * from "./models/internal-support-audit-event";
|
|
139
139
|
export * from "./models/internal-support-case";
|
|
140
140
|
export * from "./models/internal-support-security";
|
|
141
|
+
export * from "./models/security-finding";
|
|
142
|
+
export * from "./models/security-sweep-run";
|
|
143
|
+
export * from "./models/security-baseline";
|
|
144
|
+
export * from "./models/security-known-pattern";
|
|
145
|
+
export * from "./models/security-bot-controls";
|
|
146
|
+
export * from "./models/security-bot-audit-event";
|
|
147
|
+
export * from "./bot/redactor";
|
|
148
|
+
export * from "./bot/injection-markers";
|
|
149
|
+
export * from "./bot/tool";
|
|
150
|
+
export * from "./bot/gcp-logging-client";
|
|
151
|
+
export * from "./bot/k8s-client";
|
|
152
|
+
export * from "./bot/github-app-auth";
|
|
153
|
+
export * from "./bot/git-repo-cache";
|
|
154
|
+
export * from "./bot/slack-notifier";
|
|
141
155
|
export * from "./models/compliance-bot-rule";
|
|
142
156
|
export * from "./models/compliance-bot-rule-result";
|
|
143
157
|
export * from "./models/compliance-rule-set";
|
package/build/index.js
CHANGED
|
@@ -154,6 +154,20 @@ __exportStar(require("./models/internal-support-chat-message"), exports);
|
|
|
154
154
|
__exportStar(require("./models/internal-support-audit-event"), exports);
|
|
155
155
|
__exportStar(require("./models/internal-support-case"), exports);
|
|
156
156
|
__exportStar(require("./models/internal-support-security"), exports);
|
|
157
|
+
__exportStar(require("./models/security-finding"), exports);
|
|
158
|
+
__exportStar(require("./models/security-sweep-run"), exports);
|
|
159
|
+
__exportStar(require("./models/security-baseline"), exports);
|
|
160
|
+
__exportStar(require("./models/security-known-pattern"), exports);
|
|
161
|
+
__exportStar(require("./models/security-bot-controls"), exports);
|
|
162
|
+
__exportStar(require("./models/security-bot-audit-event"), exports);
|
|
163
|
+
__exportStar(require("./bot/redactor"), exports);
|
|
164
|
+
__exportStar(require("./bot/injection-markers"), exports);
|
|
165
|
+
__exportStar(require("./bot/tool"), exports);
|
|
166
|
+
__exportStar(require("./bot/gcp-logging-client"), exports);
|
|
167
|
+
__exportStar(require("./bot/k8s-client"), exports);
|
|
168
|
+
__exportStar(require("./bot/github-app-auth"), exports);
|
|
169
|
+
__exportStar(require("./bot/git-repo-cache"), exports);
|
|
170
|
+
__exportStar(require("./bot/slack-notifier"), exports);
|
|
157
171
|
__exportStar(require("./models/compliance-bot-rule"), exports);
|
|
158
172
|
__exportStar(require("./models/compliance-bot-rule-result"), exports);
|
|
159
173
|
__exportStar(require("./models/compliance-rule-set"), exports);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
export interface SecurityBaselineDoc extends mongoose.Document {
|
|
3
|
+
entityKind: string;
|
|
4
|
+
entityId: string;
|
|
5
|
+
metric: string;
|
|
6
|
+
bucket: string;
|
|
7
|
+
value: number;
|
|
8
|
+
samples: number;
|
|
9
|
+
updatedAt: Date;
|
|
10
|
+
}
|
|
11
|
+
export type SecurityBaselineModel = mongoose.Model<SecurityBaselineDoc>;
|
|
12
|
+
declare const buildSecurityBaseline: (mongoose: typeof mongoose) => SecurityBaselineModel;
|
|
13
|
+
export { buildSecurityBaseline };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSecurityBaseline = void 0;
|
|
4
|
+
const buildSecurityBaseline = (mongoose) => {
|
|
5
|
+
if (mongoose.models.SecurityBaseline) {
|
|
6
|
+
return mongoose.model("SecurityBaseline");
|
|
7
|
+
}
|
|
8
|
+
const schema = new mongoose.Schema({
|
|
9
|
+
entityKind: { type: String, required: true },
|
|
10
|
+
entityId: { type: String, required: true },
|
|
11
|
+
metric: { type: String, required: true },
|
|
12
|
+
bucket: { type: String, required: true },
|
|
13
|
+
value: { type: Number, default: 0 },
|
|
14
|
+
samples: { type: Number, default: 0 },
|
|
15
|
+
}, {
|
|
16
|
+
timestamps: { createdAt: false, updatedAt: true },
|
|
17
|
+
collection: "securityBaselines",
|
|
18
|
+
});
|
|
19
|
+
schema.index({ entityKind: 1, entityId: 1, metric: 1, bucket: 1 }, { unique: true });
|
|
20
|
+
schema.index({ updatedAt: 1 }, { expireAfterSeconds: 120 * 24 * 60 * 60 });
|
|
21
|
+
return mongoose.model("SecurityBaseline", schema);
|
|
22
|
+
};
|
|
23
|
+
exports.buildSecurityBaseline = buildSecurityBaseline;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
export declare enum SecurityBotAuditType {
|
|
3
|
+
SweepStarted = "sweepStarted",
|
|
4
|
+
SweepToolCall = "sweepToolCall",
|
|
5
|
+
SweepCompleted = "sweepCompleted",
|
|
6
|
+
SweepFailed = "sweepFailed",
|
|
7
|
+
Triage = "triage",
|
|
8
|
+
FindingStatusChanged = "findingStatusChanged",
|
|
9
|
+
RuleMuted = "ruleMuted",
|
|
10
|
+
RuleUnmuted = "ruleUnmuted",
|
|
11
|
+
ControlsChanged = "controlsChanged",
|
|
12
|
+
BudgetPaused = "budgetPaused",
|
|
13
|
+
InjectionRejected = "injectionRejected"
|
|
14
|
+
}
|
|
15
|
+
export interface SecurityBotAuditEventDoc extends mongoose.Document {
|
|
16
|
+
type: SecurityBotAuditType;
|
|
17
|
+
sweepRunId?: string;
|
|
18
|
+
findingId?: string;
|
|
19
|
+
adminAuthId?: string;
|
|
20
|
+
adminAuthEmail?: string;
|
|
21
|
+
toolName?: string;
|
|
22
|
+
toolInput?: any;
|
|
23
|
+
ok?: boolean;
|
|
24
|
+
durationMs?: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
detail?: string;
|
|
27
|
+
redactionCounts?: Record<string, number>;
|
|
28
|
+
modelId?: string;
|
|
29
|
+
inputTokens?: number;
|
|
30
|
+
outputTokens?: number;
|
|
31
|
+
estimatedUsd?: number;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
}
|
|
34
|
+
export type SecurityBotAuditEventModel = mongoose.Model<SecurityBotAuditEventDoc>;
|
|
35
|
+
declare const buildSecurityBotAuditEvent: (mongoose: typeof mongoose) => SecurityBotAuditEventModel;
|
|
36
|
+
export { buildSecurityBotAuditEvent };
|