finitemonkey 0.1.0-next.18 → 0.1.0-next.19
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/README.es.md +1 -0
- package/README.md +1 -0
- package/README.pt-BR.md +1 -0
- package/dist/finitemonkey.mjs +306 -91
- package/package.json +4 -2
- package/runbooks/anthropic.md +31 -0
- package/runbooks/asaas.md +31 -0
- package/runbooks/aws.md +32 -0
- package/runbooks/cloudflare.md +31 -0
- package/runbooks/database.md +32 -0
- package/runbooks/digitalocean.md +31 -0
- package/runbooks/generic.md +31 -0
- package/runbooks/github.md +32 -0
- package/runbooks/google.md +31 -0
- package/runbooks/mailgun.md +31 -0
- package/runbooks/openai.md +31 -0
- package/runbooks/stripe.md +31 -0
package/README.es.md
CHANGED
|
@@ -146,6 +146,7 @@ Generado desde `src/core/footprint.ts`, la lista que los tests exigen al código
|
|
|
146
146
|
| Escribe | `.finitemonkey/reports/` | Informes de error enmascarados que escribe `doctor --report`, solo cuando se pide. |
|
|
147
147
|
| Envía | `api.github.com` | A través de `gh`, solo si está instalado y con sesión iniciada: el estado del pull request indicado en una tarea y tus pull requests abiertos para la línea de la sesión. El código nunca se sube. |
|
|
148
148
|
| Envía | `127.0.0.1` | Una petición a tu propia app en el puerto que declara la comprobación `runtime`, después de arrancarla o encontrarla en marcha. Nada sale de esta máquina. |
|
|
149
|
+
| Envía | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | A través de `curl`, solo cuando tú ejecutas `finitemonkey secrets live <proveedor> --env <VARIABLE> --send`: una petición con esa clave a la API del proveedor para ver si aún funciona. Apagado por defecto, nunca lo ejecuta un hook, y el host se muestra antes de enviar nada. |
|
|
149
150
|
<!-- generated:footprint:end -->
|
|
150
151
|
|
|
151
152
|
## Preguntas frecuentes
|
package/README.md
CHANGED
|
@@ -146,6 +146,7 @@ Generated from `src/core/footprint.ts`, the list the tests hold the code to. No
|
|
|
146
146
|
| Writes | `.finitemonkey/reports/` | Masked bug reports that `doctor --report` writes, only when asked. |
|
|
147
147
|
| Sends | `api.github.com` | Through `gh`, only when it is installed and logged in: the state of the pull request named in a task, and your open pull requests for the session line. Code is never uploaded. |
|
|
148
148
|
| Sends | `127.0.0.1` | One request to your own app on the port a `runtime` check declares, after starting it or finding it running. Nothing goes past this machine. |
|
|
149
|
+
| Sends | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | Through `curl`, only when you run `finitemonkey secrets live <provider> --env <VARIABLE> --send` yourself: one request with that key to the provider's API to see whether it still works. Off by default, never run by a hook, and the host is printed before anything is sent. |
|
|
149
150
|
<!-- generated:footprint:end -->
|
|
150
151
|
|
|
151
152
|
## FAQ
|
package/README.pt-BR.md
CHANGED
|
@@ -146,6 +146,7 @@ Gerado a partir de `src/core/footprint.ts`, a lista que os testes cobram do cód
|
|
|
146
146
|
| Grava | `.finitemonkey/reports/` | Relatórios de bug mascarados que o `doctor --report` grava, só quando pedido. |
|
|
147
147
|
| Envia | `api.github.com` | Pelo `gh`, só quando ele está instalado e logado: o estado do pull request citado numa tarefa e os seus pull requests abertos para a linha da sessão. Código nunca é enviado. |
|
|
148
148
|
| Envia | `127.0.0.1` | Uma requisição ao seu próprio app na porta que a checagem `runtime` declara, depois de subir o app ou achá-lo rodando. Nada sai desta máquina. |
|
|
149
|
+
| Envia | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | Pelo `curl`, só quando você roda `finitemonkey secrets live <provedor> --env <VARIAVEL> --send`: uma requisição com aquela chave para a API do provedor para ver se ela ainda funciona. Desligado por padrão, nunca roda por hook, e o host aparece antes de qualquer envio. |
|
|
149
150
|
<!-- generated:footprint:end -->
|
|
150
151
|
|
|
151
152
|
## Perguntas frequentes
|
package/dist/finitemonkey.mjs
CHANGED
|
@@ -1156,8 +1156,8 @@ var isOwnHandler = (handler) => {
|
|
|
1156
1156
|
if (args2.length === 0) return OWN_SHELL.test(`${command} `);
|
|
1157
1157
|
const hookAt = args2.indexOf("hook");
|
|
1158
1158
|
if (hookAt < 0) return false;
|
|
1159
|
-
const
|
|
1160
|
-
return OWN_SCRIPT.test(
|
|
1159
|
+
const target2 = hookAt === 0 ? command : args2[hookAt - 1] ?? "";
|
|
1160
|
+
return OWN_SCRIPT.test(target2);
|
|
1161
1161
|
};
|
|
1162
1162
|
|
|
1163
1163
|
// src/install/skill-template.ts
|
|
@@ -3356,8 +3356,8 @@ var readRedirect = (cursor, match) => {
|
|
|
3356
3356
|
cursor.pos += match[0].length;
|
|
3357
3357
|
skipBlank(cursor);
|
|
3358
3358
|
const processSub = peek(cursor) === "(" && op === "<";
|
|
3359
|
-
const
|
|
3360
|
-
return { kind: "redirect", op, fd: match[1] ?? "", target, body: void 0 };
|
|
3359
|
+
const target2 = processSub ? "" : readWord(cursor).text;
|
|
3360
|
+
return { kind: "redirect", op, fd: match[1] ?? "", target: target2, body: void 0 };
|
|
3361
3361
|
};
|
|
3362
3362
|
var processSubstitution = (cursor) => {
|
|
3363
3363
|
if (peek(cursor) !== "<" && peek(cursor) !== ">" || peek(cursor, 1) !== "(") return false;
|
|
@@ -3536,8 +3536,8 @@ var next = (cursor, tokens, cmd) => {
|
|
|
3536
3536
|
if (redirect !== null) {
|
|
3537
3537
|
cursor.pos += redirect[0].length;
|
|
3538
3538
|
while (peek(cursor) === " ") cursor.pos += 1;
|
|
3539
|
-
const
|
|
3540
|
-
tokens.push({ kind: "redirect", op: redirect[2] ?? ">", fd: redirect[1] ?? "", target, body: void 0 });
|
|
3539
|
+
const target2 = redirect[3] ?? word(cursor, cmd).text;
|
|
3540
|
+
tokens.push({ kind: "redirect", op: redirect[2] ?? ">", fd: redirect[1] ?? "", target: target2, body: void 0 });
|
|
3541
3541
|
return;
|
|
3542
3542
|
}
|
|
3543
3543
|
const op = OPERATORS2.find((candidate) => cursor.text.startsWith(candidate, cursor.pos));
|
|
@@ -3759,8 +3759,8 @@ var PS_ENV = /^\$env:([A-Za-z_][A-Za-z0-9_]*)=?(.*)$/is;
|
|
|
3759
3759
|
var CHANGES_DIR = /* @__PURE__ */ new Set(["cd", "pushd", "set-location", "push-location"]);
|
|
3760
3760
|
var lex = (text6, language) => language === "bash" ? lexBash(text6) : lexPowerShell(text6, language === "cmd");
|
|
3761
3761
|
var isWindowsAbsolute = (path) => /^[A-Za-z]:\//.test(path);
|
|
3762
|
-
var joinDir = (current2,
|
|
3763
|
-
const next2 = toPosixPath(
|
|
3762
|
+
var joinDir = (current2, target2) => {
|
|
3763
|
+
const next2 = toPosixPath(target2);
|
|
3764
3764
|
if (next2.startsWith("/") || isWindowsAbsolute(next2) || next2.startsWith("~")) return next2;
|
|
3765
3765
|
return current2 === void 0 ? posix.normalize(next2) : posix.join(current2, next2);
|
|
3766
3766
|
};
|
|
@@ -3785,8 +3785,8 @@ var track = (words, language, context) => {
|
|
|
3785
3785
|
if (assigned !== void 0) context.env = { ...context.env, ...assigned };
|
|
3786
3786
|
const program = language === "bash" ? words[0] ?? "" : powershellProgram(words[0] ?? "");
|
|
3787
3787
|
if (!CHANGES_DIR.has(program)) return;
|
|
3788
|
-
const
|
|
3789
|
-
context.dir =
|
|
3788
|
+
const target2 = words.slice(1).find((word2) => !word2.startsWith("-"));
|
|
3789
|
+
context.dir = target2 === void 0 || target2 === "-" ? "~" : joinDir(context.dir, target2);
|
|
3790
3790
|
};
|
|
3791
3791
|
var emit = (stage, raw, language, context, collector) => {
|
|
3792
3792
|
const first = stage.argv[0] ?? "";
|
|
@@ -4295,15 +4295,15 @@ var NOT_FROM_PERSON = [
|
|
|
4295
4295
|
/\[Subagent hand-back\]/i
|
|
4296
4296
|
];
|
|
4297
4297
|
var comesFromPerson = (prompt) => !NOT_FROM_PERSON.some((pattern) => pattern.test(prompt));
|
|
4298
|
-
var toMarker = (verb,
|
|
4299
|
-
if (!NAME.test(
|
|
4300
|
-
if (verb === "trust") return { type: "trust", task:
|
|
4298
|
+
var toMarker = (verb, target2, rest) => {
|
|
4299
|
+
if (!NAME.test(target2)) return void 0;
|
|
4300
|
+
if (verb === "trust") return { type: "trust", task: target2 };
|
|
4301
4301
|
if (verb === "allow") {
|
|
4302
|
-
return rest.length >= 3 ? { type: "allow", guard:
|
|
4302
|
+
return rest.length >= 3 ? { type: "allow", guard: target2.toLowerCase(), reason: rest } : void 0;
|
|
4303
4303
|
}
|
|
4304
|
-
const slash =
|
|
4305
|
-
const task = slash < 0 ? void 0 :
|
|
4306
|
-
const check2 = slash < 0 ?
|
|
4304
|
+
const slash = target2.indexOf("/");
|
|
4305
|
+
const task = slash < 0 ? void 0 : target2.slice(0, slash);
|
|
4306
|
+
const check2 = slash < 0 ? target2 : target2.slice(slash + 1);
|
|
4307
4307
|
if (check2 === "") return void 0;
|
|
4308
4308
|
return { type: "approve", check: check2, reason: rest, ...task === void 0 || task === "" ? {} : { task } };
|
|
4309
4309
|
};
|
|
@@ -5191,9 +5191,9 @@ var prStatus = (runtime, git, paths, ttlMs = PR_CACHE_TTL_MS) => {
|
|
|
5191
5191
|
return { kind: "found", branch, pr: status.pr, localHead: headSha(git) };
|
|
5192
5192
|
};
|
|
5193
5193
|
var conflictFiles = (git, base) => {
|
|
5194
|
-
for (const
|
|
5195
|
-
if (git.out(["rev-parse", "--verify", "-q", `${
|
|
5196
|
-
const result = git.run(["merge-tree", "--write-tree", "--name-only", "--no-messages",
|
|
5194
|
+
for (const target2 of [`origin/${base}`, base]) {
|
|
5195
|
+
if (git.out(["rev-parse", "--verify", "-q", `${target2}^{commit}`]) === void 0) continue;
|
|
5196
|
+
const result = git.run(["merge-tree", "--write-tree", "--name-only", "--no-messages", target2, "HEAD"]);
|
|
5197
5197
|
if (result.found && result.exitCode === 1) return parseMergeTreeConflicts(result.stdout);
|
|
5198
5198
|
if (result.found && result.exitCode === 0) return [];
|
|
5199
5199
|
}
|
|
@@ -6196,7 +6196,17 @@ var en_default = {
|
|
|
6196
6196
|
"pause.on": "Gate resumed.",
|
|
6197
6197
|
"pause.report": "The gate did not check this stop: the person paused it with fm off at {since}.",
|
|
6198
6198
|
"pause.no_session": "No Claude Code session is recorded for this project yet; start one, then run this again or type `fm off` there.",
|
|
6199
|
-
"pause.error.person_only": "Only the person can pause or resume the gate, and this does not look like their terminal (no TTY, or running inside Claude Code)"
|
|
6199
|
+
"pause.error.person_only": "Only the person can pause or resume the gate, and this does not look like their terminal (no TTY, or running inside Claude Code)",
|
|
6200
|
+
"secrets.incident.runbook": " runbook: {link}",
|
|
6201
|
+
"secrets.command.live": "Check whether a key still works with its provider (off by default; sends only with --send, and prints the host first)",
|
|
6202
|
+
"secrets.action.live": "send a key to its provider",
|
|
6203
|
+
"secrets.live.dry": "Nothing was sent. With --send, the value of ${variable} would go to {host} ({url}) to check whether the {provider} key still works. Run it yourself, in your own terminal, with --send.",
|
|
6204
|
+
"secrets.live.contacting": "Contacting {host} ({url}) with the key; the value is passed to curl on stdin, never as an argument.",
|
|
6205
|
+
"secrets.live.live": "The key in ${variable} still works at {host} (HTTP {status}). Revoke it now: see the runbook in `finitemonkey secrets incident`.",
|
|
6206
|
+
"secrets.live.rejected": "{host} rejected the key in ${variable} (HTTP {status}): it no longer works.",
|
|
6207
|
+
"secrets.live.unknown": "{host} answered HTTP {status}; that does not say whether the key in ${variable} works. Check it in the provider's console.",
|
|
6208
|
+
"secrets.live.unreachable": "Could not reach {host} with curl, so the key in ${variable} was not checked.",
|
|
6209
|
+
"secrets.live.no_value": "${variable} is empty or not set in this terminal; export it first, nothing was sent"
|
|
6200
6210
|
};
|
|
6201
6211
|
|
|
6202
6212
|
// src/i18n/es.json
|
|
@@ -6757,7 +6767,17 @@ var es_default = {
|
|
|
6757
6767
|
"pause.on": "Compuerta reanudada.",
|
|
6758
6768
|
"pause.report": "La compuerta no revisó esta parada: la persona la pausó con fm off a las {since}.",
|
|
6759
6769
|
"pause.no_session": "Todavía no hay una sesión de Claude Code registrada en este proyecto; abre una y vuelve a ejecutarlo, o escribe `fm off` allí.",
|
|
6760
|
-
"pause.error.person_only": "Solo la persona puede pausar o reanudar la compuerta, y esto no parece su terminal (sin TTY, o dentro de Claude Code)"
|
|
6770
|
+
"pause.error.person_only": "Solo la persona puede pausar o reanudar la compuerta, y esto no parece su terminal (sin TTY, o dentro de Claude Code)",
|
|
6771
|
+
"secrets.incident.runbook": " guía: {link}",
|
|
6772
|
+
"secrets.command.live": "Comprueba si una clave aún funciona en su proveedor (apagado por defecto; solo envía con --send y muestra el host antes)",
|
|
6773
|
+
"secrets.action.live": "enviar una clave a su proveedor",
|
|
6774
|
+
"secrets.live.dry": "No se envió nada. Con --send, el valor de ${variable} iría a {host} ({url}) para comprobar si la clave de {provider} aún funciona. Ejecútalo tú, en tu terminal, con --send.",
|
|
6775
|
+
"secrets.live.contacting": "Contactando {host} ({url}) con la clave; el valor va a curl por la entrada estándar, nunca como argumento.",
|
|
6776
|
+
"secrets.live.live": "La clave en ${variable} aún funciona en {host} (HTTP {status}). Revócala ya: mira la guía en `finitemonkey secrets incident`.",
|
|
6777
|
+
"secrets.live.rejected": "{host} rechazó la clave en ${variable} (HTTP {status}): ya no funciona.",
|
|
6778
|
+
"secrets.live.unknown": "{host} respondió HTTP {status}; eso no dice si la clave en ${variable} funciona. Revísala en la consola del proveedor.",
|
|
6779
|
+
"secrets.live.unreachable": "No se pudo llegar a {host} con curl, así que la clave en ${variable} no se comprobó.",
|
|
6780
|
+
"secrets.live.no_value": "${variable} está vacía o no existe en esta terminal; expórtala antes, no se envió nada"
|
|
6761
6781
|
};
|
|
6762
6782
|
|
|
6763
6783
|
// src/i18n/pt-BR.json
|
|
@@ -7318,7 +7338,17 @@ var pt_BR_default = {
|
|
|
7318
7338
|
"pause.on": "Portão retomado.",
|
|
7319
7339
|
"pause.report": "O portão não conferiu esta parada: a pessoa pausou com fm off em {since}.",
|
|
7320
7340
|
"pause.no_session": "Ainda não há sessão do Claude Code registrada neste projeto; abra uma e rode de novo, ou digite `fm off` nela.",
|
|
7321
|
-
"pause.error.person_only": "Só a pessoa pode pausar ou retomar o portão, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)"
|
|
7341
|
+
"pause.error.person_only": "Só a pessoa pode pausar ou retomar o portão, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)",
|
|
7342
|
+
"secrets.incident.runbook": " passo a passo: {link}",
|
|
7343
|
+
"secrets.command.live": "Verifica se uma chave ainda funciona no provedor (desligado por padrão; só envia com --send e mostra o host antes)",
|
|
7344
|
+
"secrets.action.live": "enviar uma chave ao provedor",
|
|
7345
|
+
"secrets.live.dry": "Nada foi enviado. Com --send, o valor de ${variable} iria para {host} ({url}) para ver se a chave {provider} ainda funciona. Rode você mesmo, no seu terminal, com --send.",
|
|
7346
|
+
"secrets.live.contacting": "Contatando {host} ({url}) com a chave; o valor vai para o curl pela entrada padrão, nunca como argumento.",
|
|
7347
|
+
"secrets.live.live": "A chave em ${variable} ainda funciona em {host} (HTTP {status}). Revogue agora: veja o passo a passo em `finitemonkey secrets incident`.",
|
|
7348
|
+
"secrets.live.rejected": "{host} recusou a chave em ${variable} (HTTP {status}): ela não funciona mais.",
|
|
7349
|
+
"secrets.live.unknown": "{host} respondeu HTTP {status}; isso não diz se a chave em ${variable} funciona. Confira no painel do provedor.",
|
|
7350
|
+
"secrets.live.unreachable": "Não deu para alcançar {host} com o curl, então a chave em ${variable} não foi verificada.",
|
|
7351
|
+
"secrets.live.no_value": "${variable} está vazia ou não existe neste terminal; exporte antes, nada foi enviado"
|
|
7322
7352
|
};
|
|
7323
7353
|
|
|
7324
7354
|
// src/i18n/review.json
|
|
@@ -7880,7 +7910,17 @@ var review_default = {
|
|
|
7880
7910
|
"pause.on": "machine",
|
|
7881
7911
|
"pause.report": "machine",
|
|
7882
7912
|
"pause.no_session": "machine",
|
|
7883
|
-
"pause.error.person_only": "machine"
|
|
7913
|
+
"pause.error.person_only": "machine",
|
|
7914
|
+
"secrets.incident.runbook": "machine",
|
|
7915
|
+
"secrets.command.live": "machine",
|
|
7916
|
+
"secrets.action.live": "machine",
|
|
7917
|
+
"secrets.live.dry": "machine",
|
|
7918
|
+
"secrets.live.contacting": "machine",
|
|
7919
|
+
"secrets.live.live": "machine",
|
|
7920
|
+
"secrets.live.rejected": "machine",
|
|
7921
|
+
"secrets.live.unknown": "machine",
|
|
7922
|
+
"secrets.live.unreachable": "machine",
|
|
7923
|
+
"secrets.live.no_value": "machine"
|
|
7884
7924
|
},
|
|
7885
7925
|
es: {
|
|
7886
7926
|
"command.init.summary": "machine",
|
|
@@ -8439,7 +8479,17 @@ var review_default = {
|
|
|
8439
8479
|
"pause.on": "machine",
|
|
8440
8480
|
"pause.report": "machine",
|
|
8441
8481
|
"pause.no_session": "machine",
|
|
8442
|
-
"pause.error.person_only": "machine"
|
|
8482
|
+
"pause.error.person_only": "machine",
|
|
8483
|
+
"secrets.incident.runbook": "machine",
|
|
8484
|
+
"secrets.command.live": "machine",
|
|
8485
|
+
"secrets.action.live": "machine",
|
|
8486
|
+
"secrets.live.dry": "machine",
|
|
8487
|
+
"secrets.live.contacting": "machine",
|
|
8488
|
+
"secrets.live.live": "machine",
|
|
8489
|
+
"secrets.live.rejected": "machine",
|
|
8490
|
+
"secrets.live.unknown": "machine",
|
|
8491
|
+
"secrets.live.unreachable": "machine",
|
|
8492
|
+
"secrets.live.no_value": "machine"
|
|
8443
8493
|
}
|
|
8444
8494
|
};
|
|
8445
8495
|
|
|
@@ -8736,8 +8786,8 @@ var callersRate = (runtime, paths) => {
|
|
|
8736
8786
|
const parsed = line2.trim() === "" ? void 0 : parseJsonObject(line2);
|
|
8737
8787
|
if (parsed === void 0 || !parsed.ok || !isRecord(parsed.value)) continue;
|
|
8738
8788
|
const event = parsed.value["event"];
|
|
8739
|
-
const
|
|
8740
|
-
if (
|
|
8789
|
+
const target2 = event === FLAGGED_EVENT ? flagged : event === RELEASED_EVENT ? released2 : void 0;
|
|
8790
|
+
if (target2 !== void 0) for (const pair of pairs(parsed.value)) target2.add(pair);
|
|
8741
8791
|
}
|
|
8742
8792
|
const total = (/* @__PURE__ */ new Set([...flagged, ...released2])).size;
|
|
8743
8793
|
const rate = total === 0 ? 0 : released2.size / total;
|
|
@@ -8759,13 +8809,13 @@ var JS_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
|
|
|
8759
8809
|
var stripModule = (path) => path.replace(JS_EXTENSION, "").replace(/\/index$/, "");
|
|
8760
8810
|
var jsSpecifiers = (text6) => [...text6.matchAll(JS_SPECIFIER)].map((match) => match[1] ?? "");
|
|
8761
8811
|
var jsImports = (callerPath, callerText, origin) => {
|
|
8762
|
-
const
|
|
8812
|
+
const target2 = stripModule(origin);
|
|
8763
8813
|
return jsSpecifiers(callerText).some((specifier) => {
|
|
8764
8814
|
if (specifier.startsWith(".")) {
|
|
8765
|
-
return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) ===
|
|
8815
|
+
return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) === target2;
|
|
8766
8816
|
}
|
|
8767
8817
|
const bare = stripModule(specifier.replace(/^[@~#]\//, ""));
|
|
8768
|
-
return
|
|
8818
|
+
return target2 === bare || target2.endsWith(`/${bare}`);
|
|
8769
8819
|
});
|
|
8770
8820
|
};
|
|
8771
8821
|
var pyImports = (callerText, origin) => {
|
|
@@ -10283,13 +10333,13 @@ var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
|
10283
10333
|
var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
|
|
10284
10334
|
var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
|
|
10285
10335
|
var binary = (text6) => text6.slice(0, 8e3).includes("\0");
|
|
10286
|
-
var listed2 = (runtime, root,
|
|
10287
|
-
const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--",
|
|
10336
|
+
var listed2 = (runtime, root, target2) => {
|
|
10337
|
+
const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target2], root);
|
|
10288
10338
|
if (git.ok)
|
|
10289
10339
|
return accept(
|
|
10290
10340
|
git.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
|
|
10291
10341
|
);
|
|
10292
|
-
const base = toAbsolutePath(
|
|
10342
|
+
const base = toAbsolutePath(target2, root);
|
|
10293
10343
|
const walked = runtime.walkFiles(base);
|
|
10294
10344
|
if (!walked.ok) return walked;
|
|
10295
10345
|
return accept(walked.value.map((name) => toPosixPath(relative4(root, join20(base, toPosixPath(name))))));
|
|
@@ -10299,8 +10349,8 @@ var scanFile = (runtime, root, file) => {
|
|
|
10299
10349
|
if (!text6.ok || text6.value.length > MAX_FILE_BYTES || binary(text6.value)) return [];
|
|
10300
10350
|
return scanText(text6.value, { path: file }).map((finding3) => locate(finding3, file, finding3.line));
|
|
10301
10351
|
};
|
|
10302
|
-
var scanPath = (runtime, root,
|
|
10303
|
-
const files = listed2(runtime, root,
|
|
10352
|
+
var scanPath = (runtime, root, target2) => {
|
|
10353
|
+
const files = listed2(runtime, root, target2);
|
|
10304
10354
|
if (!files.ok) return files;
|
|
10305
10355
|
return accept(files.value.filter((file) => !SKIPPED.test(file)).flatMap((file) => scanFile(runtime, root, file)));
|
|
10306
10356
|
};
|
|
@@ -11320,7 +11370,7 @@ var integrationRefs = (runtime, root, defaultBranch) => [`refs/heads/${defaultBr
|
|
|
11320
11370
|
var contained = (runtime, root, branch, targets2) => {
|
|
11321
11371
|
const tip = line(runtime, root, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
11322
11372
|
if (tip === void 0) return true;
|
|
11323
|
-
return targets2.filter((
|
|
11373
|
+
return targets2.filter((target2) => target2 !== `refs/heads/${branch}`).some((target2) => runtime.git(["merge-base", "--is-ancestor", tip, target2], root).ok);
|
|
11324
11374
|
};
|
|
11325
11375
|
var shared = (runtime, root, upstream, base) => {
|
|
11326
11376
|
if (upstream === void 0 || !refExists(runtime, root, upstream.remoteRef)) return false;
|
|
@@ -12069,7 +12119,7 @@ var loadProdTargets = (runtime, root, config) => {
|
|
|
12069
12119
|
var isProdContext = (name, declared) => declared.includes(name) || /(?:^|[-_.:/])(?:prod|production)(?:$|[-_.:/])/i.test(name);
|
|
12070
12120
|
|
|
12071
12121
|
// src/prod/classify.ts
|
|
12072
|
-
var hit2 = (effect,
|
|
12122
|
+
var hit2 = (effect, target2, tool) => ({ effect, target: target2, tool });
|
|
12073
12123
|
var sshHit = (command, isProd) => {
|
|
12074
12124
|
const call = parseSsh(command);
|
|
12075
12125
|
if (call === void 0 || !isProd(call.destination)) return void 0;
|
|
@@ -12105,9 +12155,9 @@ var dockerHit = (command, targets2, isProd) => {
|
|
|
12105
12155
|
return hit2("effect", switching, "docker context");
|
|
12106
12156
|
const prodHost = remote !== void 0 && isProd(remote) ? hostKey(remote) : void 0;
|
|
12107
12157
|
const prodContext = context !== void 0 && isProdContext(context, targets2.dockerContexts) ? context : void 0;
|
|
12108
|
-
const
|
|
12109
|
-
if (
|
|
12110
|
-
return hit2(dockerReadOnly(command.args) ? "read" : "effect",
|
|
12158
|
+
const target2 = prodHost ?? prodContext;
|
|
12159
|
+
if (target2 === void 0) return void 0;
|
|
12160
|
+
return hit2(dockerReadOnly(command.args) ? "read" : "effect", target2, "docker");
|
|
12111
12161
|
};
|
|
12112
12162
|
var kubeHit = (command, targets2) => {
|
|
12113
12163
|
const context = valueAfter(command.args, "--context", "--kube-context");
|
|
@@ -12792,8 +12842,8 @@ var pushDestinations = (parsed, facts) => {
|
|
|
12792
12842
|
return [facts.upstream(current2)?.branch ?? current2];
|
|
12793
12843
|
}
|
|
12794
12844
|
return refspecs.flatMap((refspec) => {
|
|
12795
|
-
const
|
|
12796
|
-
return
|
|
12845
|
+
const target2 = refspecTarget(refspec, current2);
|
|
12846
|
+
return target2 === void 0 ? [] : [target2];
|
|
12797
12847
|
});
|
|
12798
12848
|
};
|
|
12799
12849
|
var leaseVerdict = (parsed, facts) => {
|
|
@@ -12973,8 +13023,8 @@ var guardGitCommand = (context, command, depth = 0) => {
|
|
|
12973
13023
|
if (repo === void 0) return [];
|
|
12974
13024
|
const facts = withChain(context.facts?.(repo) ?? repoFacts(context.runtime, repo), context.chain);
|
|
12975
13025
|
const expanded = expandAlias(context.runtime, repo, invocation);
|
|
12976
|
-
const
|
|
12977
|
-
if (
|
|
13026
|
+
const target2 = switchTarget(expanded, facts);
|
|
13027
|
+
if (target2 !== void 0 && context.chain !== void 0) context.chain.branch = target2;
|
|
12978
13028
|
const nested = depth < MAX_ALIAS_DEPTH ? shellAliasCommands(expanded, context.runtime, repo).flatMap(
|
|
12979
13029
|
(inner) => guardGitCommand(context, inner, depth + 1)
|
|
12980
13030
|
) : [];
|
|
@@ -13011,11 +13061,11 @@ var removalTargets = (command) => {
|
|
|
13011
13061
|
if (command.program === "remove-item") return powershellTargets(command);
|
|
13012
13062
|
return void 0;
|
|
13013
13063
|
};
|
|
13014
|
-
var globBase = (
|
|
13015
|
-
const at =
|
|
13016
|
-
if (at < 0) return
|
|
13017
|
-
const slash =
|
|
13018
|
-
return slash < 0 ? "." :
|
|
13064
|
+
var globBase = (target2) => {
|
|
13065
|
+
const at = target2.search(GLOB2);
|
|
13066
|
+
if (at < 0) return target2;
|
|
13067
|
+
const slash = target2.lastIndexOf("/", at);
|
|
13068
|
+
return slash < 0 ? "." : target2.slice(0, slash) || "/";
|
|
13019
13069
|
};
|
|
13020
13070
|
var containsGitDir = (runtime, path) => runtime.stat(toAbsolutePath(resolveFrom(path, ".git"))) !== void 0;
|
|
13021
13071
|
var targetVerdict = (runtime, context, command, raw) => {
|
|
@@ -13035,8 +13085,8 @@ var targetVerdict = (runtime, context, command, raw) => {
|
|
|
13035
13085
|
var rmVerdict = (runtime, context, command) => {
|
|
13036
13086
|
const targets2 = removalTargets(command);
|
|
13037
13087
|
if (targets2 === void 0) return PASS;
|
|
13038
|
-
for (const
|
|
13039
|
-
const verdict = targetVerdict(runtime, context, command,
|
|
13088
|
+
for (const target2 of targets2) {
|
|
13089
|
+
const verdict = targetVerdict(runtime, context, command, target2);
|
|
13040
13090
|
if (verdict.kind === "block") return verdict;
|
|
13041
13091
|
}
|
|
13042
13092
|
return PASS;
|
|
@@ -13099,7 +13149,7 @@ var appendJournal = (runtime, root, entry2) => {
|
|
|
13099
13149
|
|
|
13100
13150
|
// src/git/snapshot.ts
|
|
13101
13151
|
var BACKUP_PREFIX = "refs/finitemonkey/backup/";
|
|
13102
|
-
var DEFAULT_SNAPSHOT_TIMEOUT_MS =
|
|
13152
|
+
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 5e3;
|
|
13103
13153
|
var stampOf = (iso) => iso.replace(/[:.]/g, "-");
|
|
13104
13154
|
var slug = (op) => op.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "manual";
|
|
13105
13155
|
var optionalLine = (runtime, repo, args2) => {
|
|
@@ -13574,12 +13624,12 @@ var sourceOf = (value, isFile, segment, heredocs) => {
|
|
|
13574
13624
|
const piped = segment.args.map((arg) => heredocIn(arg, heredocs)).find((text6) => text6 !== void 0);
|
|
13575
13625
|
return { kind: "inline", text: piped ?? "" };
|
|
13576
13626
|
};
|
|
13577
|
-
var collect3 = (
|
|
13627
|
+
var collect3 = (target2, segment, args2, spec2, heredocs) => {
|
|
13578
13628
|
const values = flagValues(args2, spec2);
|
|
13579
13629
|
if (values.length === 0) return void 0;
|
|
13580
13630
|
const title = values.find((item) => spec2.title.includes(item.flag));
|
|
13581
13631
|
const bodies = values.filter((item) => !spec2.title.includes(item.flag)).map((item) => sourceOf(item.value, spec2.file.includes(item.flag), segment, heredocs));
|
|
13582
|
-
return { target, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
|
|
13632
|
+
return { target: target2, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
|
|
13583
13633
|
};
|
|
13584
13634
|
var gitSubcommand = (args2) => {
|
|
13585
13635
|
let index = 0;
|
|
@@ -13798,7 +13848,7 @@ var READERS = /* @__PURE__ */ new Set([
|
|
|
13798
13848
|
"dotenv"
|
|
13799
13849
|
]);
|
|
13800
13850
|
var SECRET_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)/i;
|
|
13801
|
-
var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b)/;
|
|
13851
|
+
var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b|live\b[^;&|\n]*\s--send\b)/;
|
|
13802
13852
|
var LISTS_ENV = /* @__PURE__ */ new Set(["get-childitem", "gci", "dir", "ls"]);
|
|
13803
13853
|
var onlyFlags = (args2) => args2.every((arg) => arg.startsWith("-"));
|
|
13804
13854
|
var readTarget = (segment) => {
|
|
@@ -13824,8 +13874,8 @@ var bashFindings = (command, segments) => {
|
|
|
13824
13874
|
const person = PERSON_ONLY.exec(command);
|
|
13825
13875
|
if (person !== null) return [{ kind: "person-only", command: person[0] }];
|
|
13826
13876
|
return segments.flatMap((segment) => {
|
|
13827
|
-
const
|
|
13828
|
-
if (
|
|
13877
|
+
const target2 = readTarget(segment);
|
|
13878
|
+
if (target2 !== void 0) return [{ kind: "read", target: target2 }];
|
|
13829
13879
|
if (dumpsEnv(segment)) return [{ kind: "env-dump", command: [segment.program, ...segment.args].join(" ") }];
|
|
13830
13880
|
return [];
|
|
13831
13881
|
});
|
|
@@ -13916,9 +13966,9 @@ var bashDecision = (input) => {
|
|
|
13916
13966
|
};
|
|
13917
13967
|
var readDecision = (input) => {
|
|
13918
13968
|
const inputs = toolInput2(input.payload);
|
|
13919
|
-
const
|
|
13920
|
-
if (
|
|
13921
|
-
return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd,
|
|
13969
|
+
const target2 = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
|
|
13970
|
+
if (target2 === "" || !isSensitivePath(target2)) return ALLOW5;
|
|
13971
|
+
return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target2) }) };
|
|
13922
13972
|
};
|
|
13923
13973
|
var WRITERS2 = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
13924
13974
|
var SHELLS4 = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
|
|
@@ -16298,15 +16348,15 @@ var problem2 = (code, messageKey, params2) => ({
|
|
|
16298
16348
|
exitCode: ExitCode.Usage
|
|
16299
16349
|
});
|
|
16300
16350
|
var underRoot = (root, path) => toPosixPath(relative8(root, path));
|
|
16301
|
-
var expand2 = (ctx, root,
|
|
16302
|
-
const path = toAbsolutePath(
|
|
16351
|
+
var expand2 = (ctx, root, target2) => {
|
|
16352
|
+
const path = toAbsolutePath(target2, ctx.cwd);
|
|
16303
16353
|
const stat = ctx.runtime.stat(path);
|
|
16304
16354
|
if (stat === void 0)
|
|
16305
|
-
return reject(problem2("prose_unreadable", "prose.error.unreadable", { file:
|
|
16355
|
+
return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target2, detail: "not found" }));
|
|
16306
16356
|
if (stat.kind === "file") return accept([underRoot(root, path)]);
|
|
16307
16357
|
const walked = ctx.runtime.walkFiles(path);
|
|
16308
16358
|
if (!walked.ok)
|
|
16309
|
-
return reject(problem2("prose_unreadable", "prose.error.unreadable", { file:
|
|
16359
|
+
return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target2, detail: walked.error }));
|
|
16310
16360
|
return accept(
|
|
16311
16361
|
walked.value.map((name) => underRoot(root, toAbsolutePath(toPosixPath(name), path))).filter((name) => TEXT_FILE.test(name) && !/(?:^|\/)(?:node_modules|\.git)\//.test(name)).sort()
|
|
16312
16362
|
);
|
|
@@ -16490,6 +16540,33 @@ var secretsAcceptCommand = {
|
|
|
16490
16540
|
}
|
|
16491
16541
|
};
|
|
16492
16542
|
|
|
16543
|
+
// src/secrets/runbooks.ts
|
|
16544
|
+
import { dirname as dirname9, join as join39 } from "node:path";
|
|
16545
|
+
var PROVIDER_OF_RULE = {
|
|
16546
|
+
"aws-access-token": "aws",
|
|
16547
|
+
"aws-secret-access-key": "aws",
|
|
16548
|
+
"github-pat": "github",
|
|
16549
|
+
"github-oauth": "github",
|
|
16550
|
+
"github-app-token": "github",
|
|
16551
|
+
"github-refresh-token": "github",
|
|
16552
|
+
"github-fine-grained-pat": "github",
|
|
16553
|
+
"stripe-access-token": "stripe",
|
|
16554
|
+
"anthropic-api-key": "anthropic",
|
|
16555
|
+
"openai-api-key": "openai",
|
|
16556
|
+
"gcp-api-key": "google",
|
|
16557
|
+
"asaas-api-key": "asaas",
|
|
16558
|
+
"credential-in-url": "database",
|
|
16559
|
+
"password-assignment": "database",
|
|
16560
|
+
"secret-in-process-args": "database"
|
|
16561
|
+
};
|
|
16562
|
+
var RUNBOOKS_URL = "https://github.com/gabrielvieiraf/finitemonkey/blob/main/runbooks";
|
|
16563
|
+
var runbookProvider = (rule) => PROVIDER_OF_RULE[rule] ?? "generic";
|
|
16564
|
+
var runbookLink = (runtime, rule) => {
|
|
16565
|
+
const file = `${runbookProvider(rule)}.md`;
|
|
16566
|
+
const local = toAbsolutePath(join39(dirname9(dirname9(runtime.selfPath)), "runbooks", file));
|
|
16567
|
+
return runtime.stat(local)?.kind === "file" ? local : `${RUNBOOKS_URL}/${file}`;
|
|
16568
|
+
};
|
|
16569
|
+
|
|
16493
16570
|
// src/commands/secrets-incident.ts
|
|
16494
16571
|
var INCIDENT_ACTIONS = ["list", "rotated", "close", "purge"];
|
|
16495
16572
|
var USAGE = "finitemonkey secrets incident [list | rotated <id> | close <id> | purge <id>]";
|
|
@@ -16515,9 +16592,19 @@ var itemLine = (ctx, incident) => ctx.t("secrets.incident.item", {
|
|
|
16515
16592
|
where: incident.where,
|
|
16516
16593
|
created: incident.createdAt.slice(0, 10)
|
|
16517
16594
|
});
|
|
16595
|
+
var itemLines2 = (ctx, incident) => [
|
|
16596
|
+
itemLine(ctx, incident),
|
|
16597
|
+
ctx.t("secrets.incident.runbook", { link: incident.runbook })
|
|
16598
|
+
];
|
|
16518
16599
|
var list2 = (ctx, root) => {
|
|
16519
|
-
const incidents = listIncidents(ctx.runtime, root)
|
|
16520
|
-
|
|
16600
|
+
const incidents = listIncidents(ctx.runtime, root).map((incident) => ({
|
|
16601
|
+
...incident,
|
|
16602
|
+
runbook: runbookLink(ctx.runtime, incident.rule)
|
|
16603
|
+
}));
|
|
16604
|
+
const human2 = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [
|
|
16605
|
+
ctx.t("secrets.incident.title", { count: incidents.length }),
|
|
16606
|
+
...incidents.flatMap((item) => itemLines2(ctx, item))
|
|
16607
|
+
];
|
|
16521
16608
|
return success({ schema: "finitemonkey/secrets-incidents@1", incidents }, human2);
|
|
16522
16609
|
};
|
|
16523
16610
|
var notRotated = (ctx, incident) => fail2(ctx, {
|
|
@@ -16584,6 +16671,133 @@ var secretsIncidentCommand = {
|
|
|
16584
16671
|
}
|
|
16585
16672
|
};
|
|
16586
16673
|
|
|
16674
|
+
// src/secrets/live.ts
|
|
16675
|
+
var bearer = (key) => [`Authorization: Bearer ${key}`];
|
|
16676
|
+
var LIVE_PROBES = {
|
|
16677
|
+
github: {
|
|
16678
|
+
host: "api.github.com",
|
|
16679
|
+
path: "/user",
|
|
16680
|
+
headers: (key) => [...bearer(key), "User-Agent: finitemonkey"]
|
|
16681
|
+
},
|
|
16682
|
+
stripe: { host: "api.stripe.com", path: "/v1/balance", headers: bearer },
|
|
16683
|
+
openai: { host: "api.openai.com", path: "/v1/models", headers: bearer },
|
|
16684
|
+
anthropic: {
|
|
16685
|
+
host: "api.anthropic.com",
|
|
16686
|
+
path: "/v1/models",
|
|
16687
|
+
headers: (key) => [`x-api-key: ${key}`, "anthropic-version: 2023-06-01"]
|
|
16688
|
+
},
|
|
16689
|
+
digitalocean: { host: "api.digitalocean.com", path: "/v2/account", headers: bearer },
|
|
16690
|
+
cloudflare: { host: "api.cloudflare.com", path: "/client/v4/user/tokens/verify", headers: bearer },
|
|
16691
|
+
asaas: {
|
|
16692
|
+
host: "api.asaas.com",
|
|
16693
|
+
path: "/v3/finance/balance",
|
|
16694
|
+
headers: (key) => [`access_token: ${key}`, "User-Agent: finitemonkey"]
|
|
16695
|
+
}
|
|
16696
|
+
};
|
|
16697
|
+
var LIVE_PROVIDERS = Object.keys(LIVE_PROBES).filter((name) => name in LIVE_PROBES);
|
|
16698
|
+
var LIVE_HOSTS = LIVE_PROVIDERS.map((name) => LIVE_PROBES[name].host);
|
|
16699
|
+
var liveProvider = (name) => LIVE_PROVIDERS.find((provider) => provider === name);
|
|
16700
|
+
var LIVE_TIMEOUT_MS = 15e3;
|
|
16701
|
+
var liveRequest = (provider, key, cwd) => {
|
|
16702
|
+
const probe = LIVE_PROBES[provider];
|
|
16703
|
+
return {
|
|
16704
|
+
command: "curl",
|
|
16705
|
+
args: [
|
|
16706
|
+
"--silent",
|
|
16707
|
+
"--show-error",
|
|
16708
|
+
"--proto",
|
|
16709
|
+
"=https",
|
|
16710
|
+
"--max-time",
|
|
16711
|
+
"10",
|
|
16712
|
+
"--header",
|
|
16713
|
+
"@-",
|
|
16714
|
+
"--write-out",
|
|
16715
|
+
"\n%{http_code}",
|
|
16716
|
+
`https://${probe.host}${probe.path}`
|
|
16717
|
+
],
|
|
16718
|
+
cwd,
|
|
16719
|
+
timeoutMs: LIVE_TIMEOUT_MS,
|
|
16720
|
+
input: `${probe.headers(key).join("\n")}
|
|
16721
|
+
`
|
|
16722
|
+
};
|
|
16723
|
+
};
|
|
16724
|
+
var liveOutcome = (result) => {
|
|
16725
|
+
if (!result.found || result.exitCode !== 0) return { verdict: "unreachable" };
|
|
16726
|
+
const status = Number.parseInt(result.stdout.trimEnd().split("\n").at(-1) ?? "", 10);
|
|
16727
|
+
if (!Number.isInteger(status) || status === 0) return { verdict: "unreachable" };
|
|
16728
|
+
if (status >= 200 && status < 300) return { verdict: "live", status };
|
|
16729
|
+
if (status === 401 || status === 403) return { verdict: "rejected", status };
|
|
16730
|
+
return { verdict: "unknown", status };
|
|
16731
|
+
};
|
|
16732
|
+
|
|
16733
|
+
// src/commands/secrets-live.ts
|
|
16734
|
+
var SECRETS_LIVE_SCHEMA = "finitemonkey/secrets-live@1";
|
|
16735
|
+
var USAGE2 = `finitemonkey secrets live <${LIVE_PROVIDERS.join("|")}> --env <VARIABLE> [--send]`;
|
|
16736
|
+
var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16737
|
+
var target = (provider) => {
|
|
16738
|
+
const probe = LIVE_PROBES[provider];
|
|
16739
|
+
return { host: probe.host, url: `https://${probe.host}${probe.path}` };
|
|
16740
|
+
};
|
|
16741
|
+
var dryRun = (ctx, provider, variable) => {
|
|
16742
|
+
const { host, url: url2 } = target(provider);
|
|
16743
|
+
const data = { schema: SECRETS_LIVE_SCHEMA, provider, host, url: url2, variable, sent: false };
|
|
16744
|
+
return success(data, [ctx.t("secrets.live.dry", { host, url: url2, variable, provider })]);
|
|
16745
|
+
};
|
|
16746
|
+
var send = (ctx, provider, variable, key) => {
|
|
16747
|
+
const { host, url: url2 } = target(provider);
|
|
16748
|
+
ctx.io.stderr.write(`${ctx.t("secrets.live.contacting", { host, url: url2 })}
|
|
16749
|
+
`);
|
|
16750
|
+
const outcome2 = liveOutcome(ctx.runtime.exec(liveRequest(provider, key, ctx.cwd)));
|
|
16751
|
+
const data = { schema: SECRETS_LIVE_SCHEMA, provider, host, url: url2, variable, sent: true, ...outcome2 };
|
|
16752
|
+
const line2 = ctx.t(`secrets.live.${outcome2.verdict}`, { host, status: outcome2.status ?? 0, variable });
|
|
16753
|
+
if (outcome2.verdict !== "unreachable") return success(data, [line2]);
|
|
16754
|
+
return failureFrom(ctx.t, {
|
|
16755
|
+
code: "secrets_live_unreachable",
|
|
16756
|
+
messageKey: "secrets.live.unreachable",
|
|
16757
|
+
params: { host, status: 0, variable },
|
|
16758
|
+
fix: "install curl or check the network, then run the command again",
|
|
16759
|
+
exitCode: ExitCode.CheckFailed,
|
|
16760
|
+
data
|
|
16761
|
+
});
|
|
16762
|
+
};
|
|
16763
|
+
var secretsLiveCommand = {
|
|
16764
|
+
path: ["secrets", "live"],
|
|
16765
|
+
summaryKey: "secrets.command.live",
|
|
16766
|
+
usage: USAGE2,
|
|
16767
|
+
status: "ready",
|
|
16768
|
+
maxPositionals: 1,
|
|
16769
|
+
options: { env: { type: "string" }, send: { type: "boolean" } },
|
|
16770
|
+
run: (ctx, args2) => {
|
|
16771
|
+
const provider = liveProvider(args2.positionals[0]);
|
|
16772
|
+
const variable = stringOption(args2.values, "env") ?? "";
|
|
16773
|
+
if (provider === void 0 || !ENV_NAME.test(variable))
|
|
16774
|
+
return Promise.resolve(
|
|
16775
|
+
failureFrom(
|
|
16776
|
+
ctx.t,
|
|
16777
|
+
usageProblem("invalid_arguments", "error.invalid_arguments", USAGE2, {
|
|
16778
|
+
detail: provider === void 0 ? args2.positionals[0] ?? "<provider>" : "--env <VARIABLE>"
|
|
16779
|
+
})
|
|
16780
|
+
)
|
|
16781
|
+
);
|
|
16782
|
+
if (args2.values["send"] !== true) return Promise.resolve(dryRun(ctx, provider, variable));
|
|
16783
|
+
if (!isPerson(ctx))
|
|
16784
|
+
return Promise.resolve(
|
|
16785
|
+
failureFrom(
|
|
16786
|
+
ctx.t,
|
|
16787
|
+
needsPersonProblem("secrets.error.person_only", `${USAGE2.replace(" [--send]", "")} --send`, {
|
|
16788
|
+
action: ctx.t("secrets.action.live")
|
|
16789
|
+
})
|
|
16790
|
+
)
|
|
16791
|
+
);
|
|
16792
|
+
const key = ctx.env[variable] ?? "";
|
|
16793
|
+
if (key === "")
|
|
16794
|
+
return Promise.resolve(
|
|
16795
|
+
failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.live.no_value", USAGE2, { variable }))
|
|
16796
|
+
);
|
|
16797
|
+
return Promise.resolve(send(ctx, provider, variable, key));
|
|
16798
|
+
}
|
|
16799
|
+
};
|
|
16800
|
+
|
|
16587
16801
|
// src/commands/secrets-scan.ts
|
|
16588
16802
|
var SECRETS_SCAN_SCHEMA = "finitemonkey/secrets-scan@1";
|
|
16589
16803
|
var view = (finding3, status) => ({
|
|
@@ -16604,15 +16818,15 @@ var modeOf3 = (values) => {
|
|
|
16604
16818
|
if (chosen[0] === true) return "staged";
|
|
16605
16819
|
return chosen[1] === true ? "history" : "path";
|
|
16606
16820
|
};
|
|
16607
|
-
var normalizedTarget = (
|
|
16608
|
-
var scopeOf = (
|
|
16609
|
-
const prefix = normalizedTarget(
|
|
16821
|
+
var normalizedTarget = (target2) => target2.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
|
|
16822
|
+
var scopeOf = (target2) => {
|
|
16823
|
+
const prefix = normalizedTarget(target2);
|
|
16610
16824
|
return (file) => prefix === "" || file === prefix || file.startsWith(`${prefix}/`);
|
|
16611
16825
|
};
|
|
16612
|
-
var runScan = async (ctx, root, mode,
|
|
16826
|
+
var runScan = async (ctx, root, mode, target2) => {
|
|
16613
16827
|
if (mode === "staged") return scanStaged(ctx.runtime, root);
|
|
16614
16828
|
if (mode === "history") return scanHistory(ctx.runtime, root);
|
|
16615
|
-
return scanPath(ctx.runtime, root, normalizedTarget(
|
|
16829
|
+
return scanPath(ctx.runtime, root, normalizedTarget(target2) || ".");
|
|
16616
16830
|
};
|
|
16617
16831
|
var historyIncidents = (ctx, root, fresh3) => fresh3.flatMap((finding3) => {
|
|
16618
16832
|
if (ruleById(finding3.ruleId)?.kind !== "credential") return [];
|
|
@@ -16693,8 +16907,8 @@ var secretsScanCommand = {
|
|
|
16693
16907
|
const fix = "finitemonkey help secrets scan";
|
|
16694
16908
|
if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
|
|
16695
16909
|
const root = projectRoot(ctx);
|
|
16696
|
-
const
|
|
16697
|
-
const scanned = await runScan(ctx, root, mode,
|
|
16910
|
+
const target2 = stringOption(args2.values, "path") ?? ".";
|
|
16911
|
+
const scanned = await runScan(ctx, root, mode, target2);
|
|
16698
16912
|
if (!scanned.ok) return failureFrom(ctx.t, gitUnavailable(scanned.error));
|
|
16699
16913
|
const baseline = loadBaseline(ctx.runtime, root);
|
|
16700
16914
|
if (!baseline.ok) return failureFrom(ctx.t, baselineInvalid(baseline.error));
|
|
@@ -16702,7 +16916,7 @@ var secretsScanCommand = {
|
|
|
16702
16916
|
scanned.value,
|
|
16703
16917
|
baseline.value,
|
|
16704
16918
|
ctx.runtime.now(),
|
|
16705
|
-
mode === "path" ? scopeOf(
|
|
16919
|
+
mode === "path" ? scopeOf(target2) : void 0
|
|
16706
16920
|
);
|
|
16707
16921
|
if (mode === "path") prune(ctx, root, baseline.value, outcome2);
|
|
16708
16922
|
const incidents = mode === "history" ? historyIncidents(ctx, root, outcome2.fresh) : [];
|
|
@@ -16711,7 +16925,7 @@ var secretsScanCommand = {
|
|
|
16711
16925
|
};
|
|
16712
16926
|
|
|
16713
16927
|
// src/task/create.ts
|
|
16714
|
-
import { join as
|
|
16928
|
+
import { join as join40 } from "node:path";
|
|
16715
16929
|
|
|
16716
16930
|
// src/core/yaml/write.ts
|
|
16717
16931
|
var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
|
|
@@ -16800,10 +17014,10 @@ var explicitChecks = (draft) => [
|
|
|
16800
17014
|
var draftChecks = (runtime, paths, draft) => {
|
|
16801
17015
|
const disk = {
|
|
16802
17016
|
read: (relative9) => {
|
|
16803
|
-
const text6 = runtime.readText(toAbsolutePath(
|
|
17017
|
+
const text6 = runtime.readText(toAbsolutePath(join40(paths.root, relative9)));
|
|
16804
17018
|
return text6.ok ? text6.value : void 0;
|
|
16805
17019
|
},
|
|
16806
|
-
exists: (relative9) => runtime.stat(toAbsolutePath(
|
|
17020
|
+
exists: (relative9) => runtime.stat(toAbsolutePath(join40(paths.root, relative9))) !== void 0
|
|
16807
17021
|
};
|
|
16808
17022
|
const detected = detectChecks(disk);
|
|
16809
17023
|
const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
|
|
@@ -16933,8 +17147,8 @@ var showTask = (ctx, id) => {
|
|
|
16933
17147
|
const paths = projectOf(ctx);
|
|
16934
17148
|
if (paths === void 0) return noRepo(ctx);
|
|
16935
17149
|
const active = readState(ctx.runtime, paths).active;
|
|
16936
|
-
const
|
|
16937
|
-
const report2 = currentReport(ctx.runtime, paths, void 0,
|
|
17150
|
+
const target2 = id ?? active;
|
|
17151
|
+
const report2 = currentReport(ctx.runtime, paths, void 0, target2);
|
|
16938
17152
|
if (report2 === void 0) {
|
|
16939
17153
|
return failureFrom(
|
|
16940
17154
|
ctx.t,
|
|
@@ -16942,7 +17156,7 @@ var showTask = (ctx, id) => {
|
|
|
16942
17156
|
"task_not_found",
|
|
16943
17157
|
"error.task_not_found",
|
|
16944
17158
|
"finitemonkey task new <id> --goal <text>",
|
|
16945
|
-
{ task:
|
|
17159
|
+
{ task: target2 ?? "-" },
|
|
16946
17160
|
ExitCode.NotConfigured
|
|
16947
17161
|
)
|
|
16948
17162
|
);
|
|
@@ -16952,7 +17166,7 @@ var showTask = (ctx, id) => {
|
|
|
16952
17166
|
var doneTask = (ctx, id) => {
|
|
16953
17167
|
const paths = projectOf(ctx);
|
|
16954
17168
|
if (paths === void 0) return noRepo(ctx);
|
|
16955
|
-
const
|
|
17169
|
+
const target2 = id ?? readState(ctx.runtime, paths).active;
|
|
16956
17170
|
const run4 = runGate({
|
|
16957
17171
|
runtime: ctx.runtime,
|
|
16958
17172
|
cwd: ctx.cwd,
|
|
@@ -16961,7 +17175,7 @@ var doneTask = (ctx, id) => {
|
|
|
16961
17175
|
budgetMs: DEFAULT_GATE_BUDGET_MS,
|
|
16962
17176
|
fast: false,
|
|
16963
17177
|
noSkip: true,
|
|
16964
|
-
...
|
|
17178
|
+
...target2 === void 0 ? {} : { taskId: target2 }
|
|
16965
17179
|
});
|
|
16966
17180
|
const result = runResult(ctx, run4, true);
|
|
16967
17181
|
if (!result.ok || run4.kind !== "done" || run4.report.implicit) return result;
|
|
@@ -17053,9 +17267,9 @@ var isEmptyDir = (project, relative9) => {
|
|
|
17053
17267
|
return listed4.ok && listed4.value.length === 0;
|
|
17054
17268
|
};
|
|
17055
17269
|
var finish3 = (project, plan) => {
|
|
17056
|
-
const
|
|
17057
|
-
const removed = project.runtime.remove(project.abs(
|
|
17058
|
-
if (!removed.ok) return reject({ path:
|
|
17270
|
+
const target2 = plan.purge ? STATE_DIR : LOCK_FILE;
|
|
17271
|
+
const removed = project.runtime.remove(project.abs(target2));
|
|
17272
|
+
if (!removed.ok) return reject({ path: target2, detail: removed.error });
|
|
17059
17273
|
for (const dir of plan.removeDirs) if (isEmptyDir(project, dir)) project.runtime.remove(project.abs(dir));
|
|
17060
17274
|
return accept(void 0);
|
|
17061
17275
|
};
|
|
@@ -17201,6 +17415,7 @@ var REGISTRY = [
|
|
|
17201
17415
|
offCommand,
|
|
17202
17416
|
onCommand,
|
|
17203
17417
|
secretsIncidentCommand,
|
|
17418
|
+
secretsLiveCommand,
|
|
17204
17419
|
proseCheckCommand,
|
|
17205
17420
|
snapshotCommand,
|
|
17206
17421
|
undoCommand,
|
|
@@ -17212,7 +17427,7 @@ var REGISTRY = [
|
|
|
17212
17427
|
];
|
|
17213
17428
|
|
|
17214
17429
|
// src/i18n/lang-settings.ts
|
|
17215
|
-
import { join as
|
|
17430
|
+
import { join as join41 } from "node:path";
|
|
17216
17431
|
var read3 = (runtime, path) => {
|
|
17217
17432
|
const text6 = runtime.readText(toAbsolutePath(path));
|
|
17218
17433
|
return text6.ok ? text6.value : void 0;
|
|
@@ -17227,14 +17442,14 @@ var languageIn = (text6) => {
|
|
|
17227
17442
|
const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
|
|
17228
17443
|
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
17229
17444
|
};
|
|
17230
|
-
var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ??
|
|
17445
|
+
var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join41(runtime.homeDir(), ".claude");
|
|
17231
17446
|
var langSettings = (runtime, cwd, env, platform) => {
|
|
17232
17447
|
const root = findGitRoot(cwd, runtime) ?? cwd;
|
|
17233
|
-
const config = parseConfigYaml(read3(runtime,
|
|
17448
|
+
const config = parseConfigYaml(read3(runtime, join41(root, CONFIG_FILE)) ?? "");
|
|
17234
17449
|
const claudeFiles = [
|
|
17235
|
-
|
|
17236
|
-
|
|
17237
|
-
|
|
17450
|
+
join41(root, ".claude", "settings.local.json"),
|
|
17451
|
+
join41(root, ".claude", "settings.json"),
|
|
17452
|
+
join41(claudeDir(runtime, env), "settings.json")
|
|
17238
17453
|
];
|
|
17239
17454
|
return {
|
|
17240
17455
|
config: configText(config, "lang"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "finitemonkey",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.19",
|
|
4
4
|
"description": "Claude Code hooks that stop a coding agent from saying done before the tests pass: a machine-checked definition of done, plus guardrails for secrets, git safety and production. Pre-release.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
36
|
+
"runbooks",
|
|
36
37
|
"LICENSE",
|
|
37
38
|
"NOTICE",
|
|
38
39
|
"README.md"
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"i18n:check": "node dist/finitemonkey.mjs i18n check --dir src/i18n",
|
|
56
57
|
"bench:startup": "node scripts/bench-startup.mjs",
|
|
57
58
|
"smoke:install": "node scripts/smoke-install.mjs",
|
|
58
|
-
"check": "npm run typecheck && npm run lint && npm run format:check && npm run check:dist && npm test && npm run i18n:check && npm run plugin:validate && npm pack --dry-run && npm run llms:check && npm run schema:check && npm run docs:check",
|
|
59
|
+
"check": "npm run typecheck && npm run lint && npm run format:check && npm run check:dist && npm test && npm run i18n:check && npm run plugin:validate && npm pack --dry-run && npm run llms:check && npm run schema:check && npm run docs:check && npm run runbooks:check",
|
|
59
60
|
"prepublishOnly": "npm run typecheck && npm run check:dist && npm test",
|
|
60
61
|
"prepare": "husky",
|
|
61
62
|
"gif": "tsc -p assets/scripts && node --experimental-strip-types assets/scripts/render.ts",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"budgets": "tsx scripts/budgets.ts",
|
|
69
70
|
"docs": "tsx scripts/build-docs.ts",
|
|
70
71
|
"docs:check": "tsc -p scripts && tsx scripts/build-docs.ts --check",
|
|
72
|
+
"runbooks:check": "tsx scripts/check-runbooks.ts",
|
|
71
73
|
"e2e:claude": "node --import tsx --test --test-concurrency=1 test/e2e/claude-stop.e2e.ts",
|
|
72
74
|
"plugin:sync": "node scripts/plugin-sync.mjs",
|
|
73
75
|
"plugin:validate": "node scripts/plugin-validate.mjs",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Anthropic API keys
|
|
2
|
+
|
|
3
|
+
Applies to rule `anthropic-api-key`.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
In the Claude Console, open **Settings** > **API keys** and disable or delete the key. With an admin key, `POST /v1/organizations/api_keys/{api_key_id}` with `{"status": "inactive"}` does the same.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Create the new key in a workspace that holds only this application, with spend limits set on that workspace. Do not reuse one key across environments.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the secret store, CI secrets and every service that reads `ANTHROPIC_API_KEY`.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Review usage and cost for the workspace in the window. Requests or spend you cannot explain mean the key was used by someone else.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
After a few days with no errors from consumers, archive or delete the disabled key.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
Prompts and files sent with the key may hold personal data. If the key could read stored files or batches, assess notice duties.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://platform.claude.com/docs/en/api/admin-api/apikeys/update-api-key (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Asaas API keys
|
|
2
|
+
|
|
3
|
+
Applies to rule `asaas-api-key`.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
As an account administrator, open **Integrações** in the web interface and delete the exposed key. A deleted key cannot be restored. A parent account can also revoke a subaccount key through the API.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Create the new key in the same area. Use a separate key per subaccount and per environment, and never put a production key in a sandbox configuration.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the backend secret store, CI secrets and every worker that calls the API.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Review charges, transfers, PIX keys, bank account changes and webhook settings for the window, in the account and in every subaccount the key could reach.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
If you created a temporary key during the switch, delete it after a few days without errors.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
Asaas keys reach customer names, tax ids and payment data. If they were read, assess notice duties under the LGPD toward the ANPD and the people affected.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://docs.asaas.com/docs/chaves-de-api (accessed 2026-10-26)
|
package/runbooks/aws.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# AWS access keys
|
|
2
|
+
|
|
3
|
+
Applies to rules `aws-access-token` and `aws-secret-access-key`.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
In the IAM console, open the user, then **Security credentials** > **Access keys**, and set the exposed key to **Inactive** (`aws iam update-access-key --access-key-id <id> --status Inactive --user-name <user>`). Inactive keys stop signing requests at once and can still be turned back on if something critical breaks.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Prefer no long-lived key at all: an IAM role for EC2, ECS or Lambda, or OIDC federation for CI (GitHub Actions `aws-actions/configure-aws-credentials` with a role). If a key is unavoidable, create it on a user whose policy grants only what the consumer needs (`aws iam create-access-key --user-name <user>`).
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the secret store, CI secrets, `.env` files outside git, and every host that had the old key. Restart the processes that read it at start.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Run `aws iam get-access-key-last-used --access-key-id <id>` and search CloudTrail for the key id from the first exposure until it went inactive. Look for new IAM users, new keys, new regions, EC2 instances you did not start and S3 reads you do not recognize.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
After a few days with no errors from consumers and no use of the old id, delete it (`aws iam delete-access-key --access-key-id <id> --user-name <user>`).
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
If CloudTrail shows access to personal data, the data protection law that applies (LGPD, GDPR or a state law) may require notice to the regulator and to the people affected. Record what was reachable with the key and decide with whoever owns that duty.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://docs.aws.amazon.com/IAM/latest/UserGuide/id-credentials-access-keys-update.html (accessed 2026-10-26)
|
|
32
|
+
- https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Cloudflare API tokens
|
|
2
|
+
|
|
3
|
+
Applies to Cloudflare API tokens and global API keys found by the generic rules.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
Go to **My Profile** > **API Tokens**, open the menu next to the token and choose **Roll**. Rolling invalidates the old secret at once and keeps the same permissions. A global API key can be changed on the same page.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Prefer a scoped API token over the global API key: only the zones and permissions the consumer needs, with client IP filtering and a TTL when the consumer runs from known addresses.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update CI secrets, Workers secrets, Terraform variables and any tool that calls the API.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Read the account audit log for the window: DNS changes, new Workers or routes, page rules, firewall changes, new members and new tokens.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
Delete any extra token created during the change once consumers have run without errors for a few days.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
DNS or Workers changes can redirect user traffic. If that happened, treat it as a breach of the affected service and assess notice duties.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://developers.cloudflare.com/fundamentals/api/how-to/roll-token/ (accessed 2026-10-26)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Database and Redis passwords
|
|
2
|
+
|
|
3
|
+
Applies to rules `credential-in-url`, `password-assignment` and `secret-in-process-args` when the value is a database or Redis password.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
PostgreSQL: `ALTER ROLE <role> WITH PASSWORD NULL;` or `ALTER ROLE <role> NOLOGIN;` until the new password is in place. MySQL: `ALTER USER '<user>'@'<host>' ACCOUNT LOCK;`. Redis with ACLs: remove the password from the user (`ACL SETUSER <user> <<old-password>`), or with `requirepass` change it in the config and restart.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Generate a long random password (`ACL GENPASS` in Redis, `openssl rand -base64 32` elsewhere), or move to IAM database authentication or short-lived credentials from a secrets manager. Give each application its own role with only the grants it needs.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update connection strings in the secret store, compose files (from a variable, never a literal argument), CI, migrations and backup jobs. Restart connection pools.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Check the server log for logins from unknown addresses, `pg_stat_activity` or `SHOW PROCESSLIST` for unknown sessions, and look for new roles, changed grants, dropped tables or large reads. Check whether the port was reachable from the internet during the window.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
Remove temporary roles or extra Redis passwords added for the switch after the applications have run cleanly for a few days.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
A database usually holds personal data. If anyone outside your team could have connected, treat it as a possible breach and assess notice duties toward the regulator and the people affected.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://www.postgresql.org/docs/17/sql-alterrole.html (accessed 2026-10-26)
|
|
32
|
+
- https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/ (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# DigitalOcean API tokens
|
|
2
|
+
|
|
3
|
+
Applies to DigitalOcean personal access tokens (`dop_v1_`) found by the generic rules.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
In the control panel, open **API** > **Tokens**, find the token and choose **Delete**, or **Regenerate** if consumers must keep the same name and scopes.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Create a token with custom scopes limited to the resources the consumer touches and an expiration date. Scopes cannot be edited later, so pick them now.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update CI secrets, `doctl auth` contexts, Terraform variables and every host that used the old token.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Review the account's security history and the resource list for the window: new Droplets, volumes, snapshots, firewall changes, new SSH keys and Spaces access keys you did not create.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
If you regenerated instead of deleting, confirm nothing still fails with the old value, then delete any leftover token created for the transition.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
If Droplets or databases with personal data were reachable, assess notice duties under the law that applies to that data.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://docs.digitalocean.com/reference/api/create-personal-access-token/ (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Any other credential
|
|
2
|
+
|
|
3
|
+
Applies to rules without a provider runbook: `generic-api-key`, `jwt`, `private-key`, `gitlab-pat`, `npm-access-token` and the Slack rules.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
Find the provider that issued the value and revoke or disable it there. For a private key, remove its public half from every place that trusts it (`authorized_keys`, deploy keys, certificates). For a JWT signing secret, rotate the secret, which invalidates every token it signed.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Prefer short-lived credentials (OIDC, roles, tokens with an expiry) over long-lived ones, and give the new value only the scope the consumer needs.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the secret store, CI secrets, `.env` files outside git and every host that had the old value.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Read the provider's audit or access log from the first exposure until revocation and look for actions you did not take.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
Delete leftover records of the old value once consumers have run without errors for a few days.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
If the credential reached personal data, assess notice duties under the law that applies.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html (accessed 2026-10-26)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# GitHub tokens
|
|
2
|
+
|
|
3
|
+
Applies to rules `github-pat`, `github-oauth`, `github-app-token`, `github-refresh-token` and `github-fine-grained-pat`.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
For a personal access token, go to **Settings** > **Developer settings** > **Personal access tokens** and delete it. For an OAuth or GitHub App token, revoke the authorization under **Settings** > **Applications**, or rotate the app's client secret. A revoked token cannot be restored.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Use the narrowest path that works: `GITHUB_TOKEN` inside Actions, a GitHub App installation token for automation, or a fine-grained personal access token limited to the repositories and permissions the consumer needs, with an expiration date.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update repository and organization secrets, CI variables, local `gh auth` sessions, git remotes that carried the token in the URL (`git remote set-url`) and credential helpers.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Check the security log (**Settings** > **Security log**, or the organization audit log) for the window: new deploy keys, new tokens, changed branch protection, pushes and releases you did not make, and repositories made public or transferred.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
A deleted token is already gone. If you only reduced an app's access, remove the old installation or secret after a few days without failures.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
If private repositories with personal data or customer code were readable, tell the owners of that data and check contractual and legal duties to notify.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation (accessed 2026-10-26)
|
|
32
|
+
- https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Google OAuth client secrets and API keys
|
|
2
|
+
|
|
3
|
+
Applies to rule `gcp-api-key` and to OAuth client secrets found by the generic rules.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
For an API key, open **APIs & Services** > **Credentials** and regenerate or delete the key. For an OAuth client secret, add a new secret to the client, then disable the exposed one.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Restrict a new API key to the APIs it calls and to the referrers, IPs or apps that use it. For OAuth, keep the client and rotate only the secret. For server-to-server access prefer workload identity federation over service account key files.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the backend configuration, CI secrets and any mobile or web build that embeds the key.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Check API usage metrics and billing for the key's project in the window, and Cloud Audit Logs for calls you do not recognize.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
Delete the disabled OAuth secret once the new one has served traffic without errors.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
If the client could read user data through granted scopes, assess notice duties toward those users and the regulator.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://support.google.com/cloud/answer/6158849 (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Mailgun API keys
|
|
2
|
+
|
|
3
|
+
Applies to Mailgun keys found by the generic rules (for example a `MAILGUN_KEY` assignment).
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
In the Mailgun control panel, open **API Security** and delete the exposed account key or domain sending key.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Prefer a domain sending key, which can only send mail for one domain, over an account key. Create an account key only for tools that manage the account, with the narrowest role.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the application configuration, CI secrets and any SMTP credentials that shared the leak.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Review sending logs and events for the window: mail you did not send, new domains, new routes or webhooks, and spikes in bounces or complaints that point to spam sent in your name.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
Remove any temporary key created for the switch after a few days without delivery errors.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
If message logs or mailing lists with personal data were readable, assess notice duties. If spam went out under your domain, warn the recipients' contact point and watch your domain reputation.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://documentation.mailgun.com/docs/mailgun/user-manual/api-key-mgmt/rbac-mgmt (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# OpenAI API keys
|
|
2
|
+
|
|
3
|
+
Applies to rule `openai-api-key`.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
On the API keys page of the OpenAI platform, revoke the exposed key. Revocation takes effect at once.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Create a project key or a service account key in a project that holds only this application, with spend limits and, where the organization allows it, a maximum key lifetime.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the secret store, CI secrets and every service that reads `OPENAI_API_KEY`.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Review usage per project and per key in the window. Calls or spend you cannot explain mean the key was used by someone else.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
Remove any temporary key created for the switch once the new key has run cleanly for a few days.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
If the key could reach stored files, assistants or fine-tuning data with personal data, assess notice duties.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://developers.openai.com/api/docs/guides/production-best-practices (accessed 2026-10-26)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Stripe secret and restricted keys
|
|
2
|
+
|
|
3
|
+
Applies to rule `stripe-access-token`.
|
|
4
|
+
|
|
5
|
+
## 1. Revoke
|
|
6
|
+
|
|
7
|
+
On **Developers** > **API keys**, open the menu next to the key and choose **Roll key** with expiration **Now**, or **Expire key**. A rolled key with a later expiration keeps working until then, so choose **Now** for a leaked key.
|
|
8
|
+
|
|
9
|
+
## 2. Issue the new credential
|
|
10
|
+
|
|
11
|
+
Create a restricted key (`rk_live_`) with only the permissions the integration needs and attach an access policy (IP addresses or allowed networks) so the key works only from your servers.
|
|
12
|
+
|
|
13
|
+
## 3. Replace it in every consumer
|
|
14
|
+
|
|
15
|
+
Update the secret store or environment of every backend, worker and CI job. Webhook signing secrets are separate; roll them too if they were in the same file.
|
|
16
|
+
|
|
17
|
+
## 4. Check for misuse in the exposure window
|
|
18
|
+
|
|
19
|
+
Open the request logs for the old key in Workbench and review the window: refunds, payouts, transfers, changes to payout bank accounts, new webhook endpoints and customer data exports.
|
|
20
|
+
|
|
21
|
+
## 5. Delete the old credential after observation
|
|
22
|
+
|
|
23
|
+
If you rolled with a delay, confirm the old key's request volume stayed at zero, then let it expire or expire it now.
|
|
24
|
+
|
|
25
|
+
## 6. Notification duties
|
|
26
|
+
|
|
27
|
+
Card data never passes through your keys, but customer names, emails and addresses do. If they were read, assess notice duties under the law that applies and tell Stripe support about suspicious activity.
|
|
28
|
+
|
|
29
|
+
## Sources
|
|
30
|
+
|
|
31
|
+
- https://docs.stripe.com/keys (accessed 2026-10-26)
|