@riocrypto/common-server 1.0.2934 → 1.0.2936
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 +15 -1
- package/build/index.js +15 -1
- package/build/models/inbound-deposit-intake.d.ts +43 -0
- package/build/models/inbound-deposit-intake.js +106 -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/package.json +3 -2
- package/build/models/fintoc-deposit-intake.d.ts +0 -39
- package/build/models/fintoc-deposit-intake.js +0 -95
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface LogEntry {
|
|
2
|
+
logName?: string;
|
|
3
|
+
timestamp?: string;
|
|
4
|
+
severity?: string;
|
|
5
|
+
insertId?: string;
|
|
6
|
+
resource?: {
|
|
7
|
+
type?: string;
|
|
8
|
+
labels?: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
labels?: Record<string, string>;
|
|
11
|
+
textPayload?: string;
|
|
12
|
+
jsonPayload?: unknown;
|
|
13
|
+
protoPayload?: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface ListEntriesRequest {
|
|
16
|
+
resourceNames: string[];
|
|
17
|
+
filter: string;
|
|
18
|
+
pageSize: number;
|
|
19
|
+
orderBy?: "timestamp asc" | "timestamp desc";
|
|
20
|
+
}
|
|
21
|
+
export declare const canObtainAccessToken: () => Promise<boolean>;
|
|
22
|
+
export declare class GcpRequestError extends Error {
|
|
23
|
+
status: number;
|
|
24
|
+
constructor(status: number, message: string);
|
|
25
|
+
}
|
|
26
|
+
export declare const listLogEntries: (request: ListEntriesRequest, timeoutMs: number) => Promise<LogEntry[]>;
|
|
@@ -0,0 +1,127 @@
|
|
|
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.listLogEntries = exports.GcpRequestError = exports.canObtainAccessToken = void 0;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const google_auth_library_1 = require("google-auth-library");
|
|
18
|
+
// Minimal Cloud Logging REST client. Only entries:list is implemented on
|
|
19
|
+
// purpose. Auth comes from Application Default Credentials (Workload
|
|
20
|
+
// Identity in the cluster, GOOGLE_APPLICATION_CREDENTIALS locally) and the
|
|
21
|
+
// token is requested with the read-only logging scope, so even a bug in this
|
|
22
|
+
// file cannot turn into a write.
|
|
23
|
+
const LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
|
|
24
|
+
const ENTRIES_LIST_URL = "https://logging.googleapis.com/v2/entries:list";
|
|
25
|
+
let auth = null;
|
|
26
|
+
const getAuth = () => {
|
|
27
|
+
if (!auth) {
|
|
28
|
+
auth = new google_auth_library_1.GoogleAuth({ scopes: [LOGGING_READ_SCOPE] });
|
|
29
|
+
}
|
|
30
|
+
return auth;
|
|
31
|
+
};
|
|
32
|
+
// Used locally to decide whether to register the tool at all. In the cluster
|
|
33
|
+
// Workload Identity always yields a token; on a laptop it depends on whether
|
|
34
|
+
// the engineer has Application Default Credentials.
|
|
35
|
+
const canObtainAccessToken = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
|
+
try {
|
|
37
|
+
const client = yield getAuth().getClient();
|
|
38
|
+
const { token } = yield client.getAccessToken();
|
|
39
|
+
return Boolean(token);
|
|
40
|
+
}
|
|
41
|
+
catch (_a) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
exports.canObtainAccessToken = canObtainAccessToken;
|
|
46
|
+
// Cloud Logging reads are quota limited per minute. Scripts paging through
|
|
47
|
+
// time windows fire many requests back to back, so requests are serialised
|
|
48
|
+
// with a minimum spacing and 429s are retried with backoff. One process, one
|
|
49
|
+
// queue; the tool timeout still bounds the total wait.
|
|
50
|
+
const MIN_SPACING_MS = 1100;
|
|
51
|
+
const MAX_ATTEMPTS = 4;
|
|
52
|
+
let lastRequestAt = 0;
|
|
53
|
+
let queue = Promise.resolve();
|
|
54
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
+
const throttled = (fn) => {
|
|
56
|
+
const run = queue.then(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
const wait = lastRequestAt + MIN_SPACING_MS - Date.now();
|
|
58
|
+
if (wait > 0)
|
|
59
|
+
yield sleep(wait);
|
|
60
|
+
lastRequestAt = Date.now();
|
|
61
|
+
return fn();
|
|
62
|
+
}));
|
|
63
|
+
queue = run.then(() => undefined, () => undefined);
|
|
64
|
+
return run;
|
|
65
|
+
};
|
|
66
|
+
const gcpErrorMessage = (error) => {
|
|
67
|
+
var _a, _b, _c;
|
|
68
|
+
const message = (_c = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error) === null || _c === void 0 ? void 0 : _c.message;
|
|
69
|
+
return typeof message === "string" ? message.slice(0, 400) : undefined;
|
|
70
|
+
};
|
|
71
|
+
class GcpRequestError extends Error {
|
|
72
|
+
constructor(status, message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.status = status;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.GcpRequestError = GcpRequestError;
|
|
78
|
+
const listLogEntries = (request, timeoutMs) => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
var _b, _c, _d, _e;
|
|
80
|
+
const client = yield getAuth().getClient();
|
|
81
|
+
const { token } = yield client.getAccessToken();
|
|
82
|
+
if (!token) {
|
|
83
|
+
throw new Error("Could not obtain a GCP access token");
|
|
84
|
+
}
|
|
85
|
+
const body = {
|
|
86
|
+
resourceNames: request.resourceNames,
|
|
87
|
+
filter: request.filter,
|
|
88
|
+
pageSize: request.pageSize,
|
|
89
|
+
orderBy: request.orderBy || "timestamp desc",
|
|
90
|
+
};
|
|
91
|
+
const config = {
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: `Bearer ${token}`,
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
},
|
|
96
|
+
timeout: timeoutMs,
|
|
97
|
+
maxContentLength: 8 * 1024 * 1024,
|
|
98
|
+
maxRedirects: 0,
|
|
99
|
+
};
|
|
100
|
+
for (let attempt = 1;; attempt++) {
|
|
101
|
+
try {
|
|
102
|
+
const response = yield throttled(() => axios_1.default.post(ENTRIES_LIST_URL, body, config));
|
|
103
|
+
return Array.isArray((_b = response.data) === null || _b === void 0 ? void 0 : _b.entries) ? response.data.entries : [];
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const status = (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.status;
|
|
107
|
+
if (status === 429 && attempt < MAX_ATTEMPTS) {
|
|
108
|
+
// Quota windows are a minute long; short retries just burn attempts.
|
|
109
|
+
const retryAfter = Number((_e = (_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.headers) === null || _e === void 0 ? void 0 : _e["retry-after"]);
|
|
110
|
+
const backoff = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 5000 * 2 ** (attempt - 1);
|
|
111
|
+
yield sleep(Math.min(backoff, 25000));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (status === 429) {
|
|
115
|
+
throw new GcpRequestError(429, "Cloud Logging read quota exceeded. Wait a few seconds and use fewer, wider queries (one 2 hour query instead of eight 15 minute ones).");
|
|
116
|
+
}
|
|
117
|
+
if (status === 400) {
|
|
118
|
+
throw new GcpRequestError(400, `Cloud Logging rejected the filter: ${gcpErrorMessage(error) || "invalid request"}`);
|
|
119
|
+
}
|
|
120
|
+
if (status !== undefined) {
|
|
121
|
+
throw new GcpRequestError(status, `Cloud Logging returned ${status}: ${gcpErrorMessage(error) || (error === null || error === void 0 ? void 0 : error.message) || "request failed"}`);
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
exports.listLogEntries = listLogEntries;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { GitHubAppAuth } from "./github-app-auth";
|
|
2
|
+
export interface GitResult {
|
|
3
|
+
stdout: string;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface GitRepoCacheOptions {
|
|
7
|
+
org: string;
|
|
8
|
+
cacheDir: string;
|
|
9
|
+
auth: GitHubAppAuth;
|
|
10
|
+
historyDays: number;
|
|
11
|
+
refreshIntervalMs: number;
|
|
12
|
+
gitTimeoutMs: number;
|
|
13
|
+
cloneTimeoutMs: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const isValidRepoName: (name: string) => boolean;
|
|
16
|
+
export declare const isValidRef: (ref: string) => boolean;
|
|
17
|
+
export declare class GitRepoCache {
|
|
18
|
+
private readonly options;
|
|
19
|
+
private repoLocks;
|
|
20
|
+
private lastRefresh;
|
|
21
|
+
constructor(options: GitRepoCacheOptions);
|
|
22
|
+
private repoUrl;
|
|
23
|
+
private repoDir;
|
|
24
|
+
private runGit;
|
|
25
|
+
private withRepoLock;
|
|
26
|
+
private isCloned;
|
|
27
|
+
private clone;
|
|
28
|
+
private refreshIfStale;
|
|
29
|
+
private refExists;
|
|
30
|
+
private fetchRef;
|
|
31
|
+
ensureRepoRef(repo: string, requestedRef?: string): Promise<string>;
|
|
32
|
+
gitInRepo(repo: string, args: string[], maxBuffer?: number): Promise<GitResult>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.GitRepoCache = exports.isValidRef = exports.isValidRepoName = void 0;
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const fs_1 = require("fs");
|
|
15
|
+
const path_1 = require("path");
|
|
16
|
+
const tool_1 = require("./tool");
|
|
17
|
+
// Bare, shallow, per-repo clones under a writable directory. Repos are
|
|
18
|
+
// cloned the first time a tool asks for them and refreshed when older than
|
|
19
|
+
// refreshIntervalMs. Everything is read through git plumbing against refs
|
|
20
|
+
// (git grep / git show / git log), so there is never a working tree and
|
|
21
|
+
// never a checkout to keep in sync.
|
|
22
|
+
//
|
|
23
|
+
// Git is a subprocess fed by model input. Every argument is validated by
|
|
24
|
+
// the caller, passed as an array (never a shell string), and user-supplied
|
|
25
|
+
// values always follow "--" or "-e" so they cannot be read as options.
|
|
26
|
+
const REPO_NAME = /^[a-z0-9][a-z0-9-]{0,49}$/;
|
|
27
|
+
const VERSION_TAG = /^v?\d+\.\d+\.\d+(?:[-+.][A-Za-z0-9.-]{1,40})?$/;
|
|
28
|
+
const SHORT_SHA = /^[0-9a-f]{7,40}$/;
|
|
29
|
+
const DEFAULT_REF = "HEAD";
|
|
30
|
+
const isValidRepoName = (name) => REPO_NAME.test(name);
|
|
31
|
+
exports.isValidRepoName = isValidRepoName;
|
|
32
|
+
const isValidRef = (ref) => ref === DEFAULT_REF || VERSION_TAG.test(ref) || SHORT_SHA.test(ref);
|
|
33
|
+
exports.isValidRef = isValidRef;
|
|
34
|
+
const basicHeaderValue = (token) => Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
35
|
+
// Removes both the raw token and the base64 basic auth header, which is what
|
|
36
|
+
// execFile's error.message carries when it echoes the argv.
|
|
37
|
+
const scrub = (text, token) => token
|
|
38
|
+
? text
|
|
39
|
+
.split(token)
|
|
40
|
+
.join("[REDACTED_GITHUB_TOKEN]")
|
|
41
|
+
.split(basicHeaderValue(token))
|
|
42
|
+
.join("[REDACTED_GITHUB_TOKEN]")
|
|
43
|
+
: text;
|
|
44
|
+
class GitRepoCache {
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
this.repoLocks = new Map();
|
|
48
|
+
this.lastRefresh = new Map();
|
|
49
|
+
}
|
|
50
|
+
repoUrl(repo) {
|
|
51
|
+
return `https://github.com/${this.options.org}/${repo}.git`;
|
|
52
|
+
}
|
|
53
|
+
repoDir(repo) {
|
|
54
|
+
return (0, path_1.join)(this.options.cacheDir, `${repo}.git`);
|
|
55
|
+
}
|
|
56
|
+
runGit(args, cwd, timeoutMs, token, maxBuffer = 4 * 1024 * 1024) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
// The token travels as a per-invocation header, never into .git/config.
|
|
59
|
+
const authArgs = token
|
|
60
|
+
? ["-c", `http.extraheader=AUTHORIZATION: basic ${basicHeaderValue(token)}`]
|
|
61
|
+
: [];
|
|
62
|
+
const fullArgs = [
|
|
63
|
+
"-c",
|
|
64
|
+
"credential.helper=",
|
|
65
|
+
"-c",
|
|
66
|
+
"core.askPass=",
|
|
67
|
+
"-c",
|
|
68
|
+
"protocol.allow=never",
|
|
69
|
+
"-c",
|
|
70
|
+
"protocol.https.allow=always",
|
|
71
|
+
...authArgs,
|
|
72
|
+
...args,
|
|
73
|
+
];
|
|
74
|
+
(0, child_process_1.execFile)("git", fullArgs, {
|
|
75
|
+
cwd,
|
|
76
|
+
timeout: timeoutMs,
|
|
77
|
+
maxBuffer,
|
|
78
|
+
env: {
|
|
79
|
+
PATH: process.env.PATH || "/usr/bin:/bin",
|
|
80
|
+
HOME: this.options.cacheDir,
|
|
81
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
82
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
83
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
84
|
+
LC_ALL: "C",
|
|
85
|
+
},
|
|
86
|
+
}, (error, stdout, stderr) => {
|
|
87
|
+
if (error) {
|
|
88
|
+
// git grep exits 1 on "no matches", which is not an error here.
|
|
89
|
+
if (error.code === 1 && args.includes("grep")) {
|
|
90
|
+
return resolve({ stdout: "", truncated: false });
|
|
91
|
+
}
|
|
92
|
+
if (error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
93
|
+
return resolve({ stdout: String(stdout || ""), truncated: true });
|
|
94
|
+
}
|
|
95
|
+
const detail = scrub(String(stderr || error.message || "git failed"), token)
|
|
96
|
+
.split("\n")
|
|
97
|
+
.filter((l) => l.trim().length > 0)
|
|
98
|
+
.slice(0, 3)
|
|
99
|
+
.join(" ");
|
|
100
|
+
if (error.killed || error.signal === "SIGTERM") {
|
|
101
|
+
return reject(new tool_1.ToolInputError(`git timed out after ${timeoutMs / 1000}s`));
|
|
102
|
+
}
|
|
103
|
+
return reject(new Error(detail));
|
|
104
|
+
}
|
|
105
|
+
resolve({ stdout: String(stdout || ""), truncated: false });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
withRepoLock(repo, fn) {
|
|
110
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
const previous = this.repoLocks.get(repo) || Promise.resolve();
|
|
112
|
+
const next = previous.catch(() => undefined).then(fn);
|
|
113
|
+
this.repoLocks.set(repo, next);
|
|
114
|
+
try {
|
|
115
|
+
return yield next;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (this.repoLocks.get(repo) === next)
|
|
119
|
+
this.repoLocks.delete(repo);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
isCloned(repo) {
|
|
124
|
+
return (0, fs_1.existsSync)((0, path_1.join)(this.repoDir(repo), "HEAD"));
|
|
125
|
+
}
|
|
126
|
+
clone(repo) {
|
|
127
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
128
|
+
(0, fs_1.mkdirSync)(this.options.cacheDir, { recursive: true });
|
|
129
|
+
const token = yield this.options.auth.getInstallationToken();
|
|
130
|
+
try {
|
|
131
|
+
yield this.runGit([
|
|
132
|
+
"clone",
|
|
133
|
+
"--bare",
|
|
134
|
+
"--single-branch",
|
|
135
|
+
"--no-tags",
|
|
136
|
+
"--depth",
|
|
137
|
+
String(this.options.historyDays * 10),
|
|
138
|
+
this.repoUrl(repo),
|
|
139
|
+
this.repoDir(repo),
|
|
140
|
+
], undefined, this.options.cloneTimeoutMs, token);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const message = String((error === null || error === void 0 ? void 0 : error.message) || "");
|
|
144
|
+
if (/not found|Repository not found|403|404/i.test(message)) {
|
|
145
|
+
throw new tool_1.ToolInputError(`repository "${repo}" was not found or the GitHub App is not installed on it`);
|
|
146
|
+
}
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
this.lastRefresh.set(repo, Date.now());
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
refreshIfStale(repo) {
|
|
153
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
const last = this.lastRefresh.get(repo) || 0;
|
|
155
|
+
if (Date.now() - last < this.options.refreshIntervalMs)
|
|
156
|
+
return;
|
|
157
|
+
// A bare --single-branch clone has no fetch refspec, so name the branch
|
|
158
|
+
// HEAD points at explicitly or the fetch would only update FETCH_HEAD.
|
|
159
|
+
const head = yield this.runGit(["symbolic-ref", "--quiet", "HEAD"], this.repoDir(repo), this.options.gitTimeoutMs);
|
|
160
|
+
const branch = head.stdout.trim();
|
|
161
|
+
if (!/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(branch)) {
|
|
162
|
+
throw new Error(`unexpected HEAD in ${repo}`);
|
|
163
|
+
}
|
|
164
|
+
const token = yield this.options.auth.getInstallationToken();
|
|
165
|
+
yield this.runGit([
|
|
166
|
+
"fetch",
|
|
167
|
+
"--no-tags",
|
|
168
|
+
"--depth",
|
|
169
|
+
String(this.options.historyDays * 10),
|
|
170
|
+
"origin",
|
|
171
|
+
`+${branch}:${branch}`,
|
|
172
|
+
], this.repoDir(repo), this.options.gitTimeoutMs, token);
|
|
173
|
+
this.lastRefresh.set(repo, Date.now());
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
refExists(repo, ref) {
|
|
177
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
178
|
+
try {
|
|
179
|
+
yield this.runGit(["rev-parse", "--verify", "--quiet", "--end-of-options", `${ref}^{commit}`], this.repoDir(repo), this.options.gitTimeoutMs);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (_a) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
fetchRef(repo, refspec) {
|
|
188
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
189
|
+
const token = yield this.options.auth.getInstallationToken();
|
|
190
|
+
try {
|
|
191
|
+
yield this.runGit(["fetch", "--depth", "1", "origin", refspec], this.repoDir(repo), this.options.gitTimeoutMs, token);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch (_a) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Makes sure the repo is cloned and the requested ref is present locally.
|
|
200
|
+
// Returns the fully qualified ref to pass to git plumbing.
|
|
201
|
+
ensureRepoRef(repo, requestedRef) {
|
|
202
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
203
|
+
if (!(0, exports.isValidRepoName)(repo)) {
|
|
204
|
+
throw new tool_1.ToolInputError(`"${repo}" is not a valid repository name`);
|
|
205
|
+
}
|
|
206
|
+
const ref = requestedRef || DEFAULT_REF;
|
|
207
|
+
if (!(0, exports.isValidRef)(ref)) {
|
|
208
|
+
throw new tool_1.ToolInputError(`"ref" must be HEAD, a version tag like v1.2.3, or a commit sha`);
|
|
209
|
+
}
|
|
210
|
+
return this.withRepoLock(repo, () => __awaiter(this, void 0, void 0, function* () {
|
|
211
|
+
if (!this.isCloned(repo)) {
|
|
212
|
+
yield this.clone(repo);
|
|
213
|
+
}
|
|
214
|
+
if (ref === DEFAULT_REF) {
|
|
215
|
+
yield this.refreshIfStale(repo);
|
|
216
|
+
return DEFAULT_REF;
|
|
217
|
+
}
|
|
218
|
+
if (VERSION_TAG.test(ref)) {
|
|
219
|
+
const candidates = ref.startsWith("v") ? [ref, ref.slice(1)] : [ref, `v${ref}`];
|
|
220
|
+
for (const candidate of candidates) {
|
|
221
|
+
const qualified = `refs/tags/${candidate}`;
|
|
222
|
+
if (yield this.refExists(repo, qualified))
|
|
223
|
+
return qualified;
|
|
224
|
+
if ((yield this.fetchRef(repo, `+refs/tags/${candidate}:refs/tags/${candidate}`)) &&
|
|
225
|
+
(yield this.refExists(repo, qualified))) {
|
|
226
|
+
return qualified;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw new tool_1.ToolInputError(`tag "${ref}" does not exist in ${repo}. Use HEAD for the default branch.`);
|
|
230
|
+
}
|
|
231
|
+
if (yield this.refExists(repo, ref))
|
|
232
|
+
return ref;
|
|
233
|
+
if ((yield this.fetchRef(repo, ref)) && (yield this.refExists(repo, ref)))
|
|
234
|
+
return ref;
|
|
235
|
+
throw new tool_1.ToolInputError(`commit "${ref}" does not exist in ${repo}`);
|
|
236
|
+
}));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Read-only plumbing. Callers validate every argument before reaching here.
|
|
240
|
+
gitInRepo(repo, args, maxBuffer) {
|
|
241
|
+
return this.runGit(args, this.repoDir(repo), this.options.gitTimeoutMs, undefined, maxBuffer);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.GitRepoCache = GitRepoCache;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface GitHubAppAuthOptions {
|
|
2
|
+
appId: string;
|
|
3
|
+
installationId: string;
|
|
4
|
+
privateKey: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const buildAppJwt: (appId: string, privateKey: string, nowSeconds: number) => string;
|
|
7
|
+
export declare class GitHubAppAuth {
|
|
8
|
+
private readonly options;
|
|
9
|
+
private cached;
|
|
10
|
+
private inFlight;
|
|
11
|
+
constructor(options: GitHubAppAuthOptions);
|
|
12
|
+
private mint;
|
|
13
|
+
getInstallationToken(): Promise<string>;
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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.GitHubAppAuth = exports.buildAppJwt = void 0;
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
const crypto_1 = require("crypto");
|
|
18
|
+
// GitHub App authentication. The App holds only Contents:read and
|
|
19
|
+
// Metadata:read. Its private key is the one long-lived secret; it never
|
|
20
|
+
// leaves the process. Installation tokens minted from it live for an hour
|
|
21
|
+
// and are the only thing git ever sees.
|
|
22
|
+
const GITHUB_API = "https://api.github.com";
|
|
23
|
+
const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
24
|
+
const base64Url = (input) => Buffer.from(input)
|
|
25
|
+
.toString("base64")
|
|
26
|
+
.replace(/=/g, "")
|
|
27
|
+
.replace(/\+/g, "-")
|
|
28
|
+
.replace(/\//g, "_");
|
|
29
|
+
const buildAppJwt = (appId, privateKey, nowSeconds) => {
|
|
30
|
+
const header = base64Url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
31
|
+
// iat backdated 60s to tolerate clock drift, exp is GitHub's 10 minute max.
|
|
32
|
+
const payload = base64Url(JSON.stringify({ iat: nowSeconds - 60, exp: nowSeconds + 9 * 60, iss: appId }));
|
|
33
|
+
const signer = (0, crypto_1.createSign)("RSA-SHA256");
|
|
34
|
+
signer.update(`${header}.${payload}`);
|
|
35
|
+
const signature = base64Url(signer.sign(privateKey));
|
|
36
|
+
return `${header}.${payload}.${signature}`;
|
|
37
|
+
};
|
|
38
|
+
exports.buildAppJwt = buildAppJwt;
|
|
39
|
+
class GitHubAppAuth {
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.cached = null;
|
|
43
|
+
this.inFlight = null;
|
|
44
|
+
}
|
|
45
|
+
mint() {
|
|
46
|
+
var _a;
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const jwt = (0, exports.buildAppJwt)(this.options.appId, this.options.privateKey, Math.floor(Date.now() / 1000));
|
|
49
|
+
const response = yield axios_1.default.post(`${GITHUB_API}/app/installations/${encodeURIComponent(this.options.installationId)}/access_tokens`,
|
|
50
|
+
// Ask for the minimum even if the App were ever granted more.
|
|
51
|
+
{ permissions: { contents: "read", metadata: "read" } }, {
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${jwt}`,
|
|
54
|
+
Accept: "application/vnd.github+json",
|
|
55
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
56
|
+
},
|
|
57
|
+
timeout: 15000,
|
|
58
|
+
maxRedirects: 0,
|
|
59
|
+
});
|
|
60
|
+
const token = (_a = response.data) === null || _a === void 0 ? void 0 : _a.token;
|
|
61
|
+
if (!token) {
|
|
62
|
+
throw new Error("GitHub did not return an installation token");
|
|
63
|
+
}
|
|
64
|
+
const expiresAt = response.data.expires_at
|
|
65
|
+
? Date.parse(response.data.expires_at)
|
|
66
|
+
: Date.now() + 60 * 60 * 1000;
|
|
67
|
+
this.cached = { token, expiresAt };
|
|
68
|
+
return token;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
getInstallationToken() {
|
|
72
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
if (this.cached && this.cached.expiresAt - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
|
|
74
|
+
return this.cached.token;
|
|
75
|
+
}
|
|
76
|
+
if (!this.inFlight) {
|
|
77
|
+
this.inFlight = this.mint().then((token) => {
|
|
78
|
+
this.inFlight = null;
|
|
79
|
+
return token;
|
|
80
|
+
}, (error) => {
|
|
81
|
+
this.inFlight = null;
|
|
82
|
+
throw error;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return this.inFlight;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
reset() {
|
|
89
|
+
this.cached = null;
|
|
90
|
+
this.inFlight = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.GitHubAppAuth = GitHubAppAuth;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const hasInjectionMarker: (text: string) => boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasInjectionMarker = void 0;
|
|
4
|
+
// Instruction-like text inside tool output. Kept narrow on purpose: a false
|
|
5
|
+
// positive can lock an admin out or reject a whole sweep report, a miss is
|
|
6
|
+
// just an alert not sent.
|
|
7
|
+
const INJECTION_MARKERS = [
|
|
8
|
+
/ignore\s+(all\s+|the\s+|any\s+)?(previous|prior|above|earlier)\s+(instructions|rules|prompts?)/i,
|
|
9
|
+
/disregard\s+(your|the|all)\s+(system|previous|prior)\s+(prompt|instructions|rules)/i,
|
|
10
|
+
/(reveal|print|output|show|leak|dump)\s+(me\s+)?(the|your)\s+(system\s+prompt|api\s+key|secret|token|credentials?|private\s+key)/i,
|
|
11
|
+
/you\s+are\s+now\s+(in\s+)?(developer|debug|admin|unrestricted|dan)\s+mode/i,
|
|
12
|
+
/<\/?(system|assistant|tool_result_untrusted_data|past_cases_untrusted_data|memory_untrusted_data)>/i,
|
|
13
|
+
/\bBEGIN\s+SYSTEM\s+PROMPT\b/i,
|
|
14
|
+
];
|
|
15
|
+
const hasInjectionMarker = (text) => INJECTION_MARKERS.some((re) => re.test(text));
|
|
16
|
+
exports.hasInjectionMarker = hasInjectionMarker;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
export interface PodSummary {
|
|
4
|
+
name: string;
|
|
5
|
+
phase: string;
|
|
6
|
+
ready: string;
|
|
7
|
+
restarts: number;
|
|
8
|
+
ageMinutes: number;
|
|
9
|
+
containers: string[];
|
|
10
|
+
node?: string;
|
|
11
|
+
lastState?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DeploymentSummary {
|
|
14
|
+
name: string;
|
|
15
|
+
desired: number;
|
|
16
|
+
ready: number;
|
|
17
|
+
available: number;
|
|
18
|
+
updated: number;
|
|
19
|
+
images: string[];
|
|
20
|
+
conditions: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface EventSummary {
|
|
23
|
+
lastSeen: string;
|
|
24
|
+
type: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
object: string;
|
|
27
|
+
count: number;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class K8sClient {
|
|
31
|
+
private http;
|
|
32
|
+
constructor(baseURL: string, ca: Buffer | undefined, timeoutMs: number);
|
|
33
|
+
static fromInCluster(timeoutMs: number): K8sClient | null;
|
|
34
|
+
private headers;
|
|
35
|
+
private get;
|
|
36
|
+
listPods(namespace: string): Promise<PodSummary[]>;
|
|
37
|
+
getPodLog(namespace: string, pod: string, options: {
|
|
38
|
+
container?: string;
|
|
39
|
+
tailLines: number;
|
|
40
|
+
sinceSeconds: number;
|
|
41
|
+
previous: boolean;
|
|
42
|
+
}): Promise<string>;
|
|
43
|
+
listDeployments(namespace: string): Promise<DeploymentSummary[]>;
|
|
44
|
+
listEvents(namespace: string, involvedObjectName?: string): Promise<EventSummary[]>;
|
|
45
|
+
}
|