@cristobalbn/chaski 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +376 -0
- package/CHANGELOG.md +289 -0
- package/LICENSE +21 -0
- package/README.md +588 -2
- package/SECURITY.md +75 -0
- package/package.json +48 -4
- package/skill/SKILL.md +80 -0
- package/src/capturar.mjs +24 -0
- package/src/capture.mjs +545 -0
- package/src/cli.mjs +1053 -0
- package/src/inject.mjs +199 -0
- package/src/leer.mjs +19 -0
- package/src/lib/ask.mjs +194 -0
- package/src/lib/budget.mjs +64 -0
- package/src/lib/facts.mjs +566 -0
- package/src/lib/paths.mjs +323 -0
- package/src/lib/relay.mjs +303 -0
- package/src/lib/secrets.mjs +148 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Filtro de secretos.
|
|
2
|
+
//
|
|
3
|
+
// Corre ANTES de escribir, no solo antes de enviar: un token en
|
|
4
|
+
// .tambo/relevo.md ya es una fuga aunque nunca salga de la máquina.
|
|
5
|
+
//
|
|
6
|
+
// Lo redactado deja marca visible para que nadie confunda una redacción con
|
|
7
|
+
// un dato ausente.
|
|
8
|
+
|
|
9
|
+
const PATTERNS = [
|
|
10
|
+
// Credenciales con prefijo reconocible
|
|
11
|
+
[/\bsk-[A-Za-z0-9_-]{16,}\b/g, 'clave-api'],
|
|
12
|
+
[/\bsk-ant-[A-Za-z0-9_-]{16,}\b/g, 'clave-anthropic'],
|
|
13
|
+
[/\bAIza[0-9A-Za-z_-]{30,}\b/g, 'clave-google'],
|
|
14
|
+
[/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, 'token-github'],
|
|
15
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, 'token-github'],
|
|
16
|
+
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, 'token-slack'],
|
|
17
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, 'clave-aws'],
|
|
18
|
+
[/\bASIA[0-9A-Z]{16}\b/g, 'clave-aws-temporal'],
|
|
19
|
+
[/\bglpat-[A-Za-z0-9_-]{16,}\b/g, 'token-gitlab'],
|
|
20
|
+
[/\bnpm_[A-Za-z0-9]{30,}\b/g, 'token-npm'],
|
|
21
|
+
[/\bdop_v1_[a-f0-9]{32,}\b/g, 'token-digitalocean'],
|
|
22
|
+
[/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, 'clave-sendgrid'],
|
|
23
|
+
[/\bsk_live_[A-Za-z0-9]{16,}\b/g, 'clave-stripe'],
|
|
24
|
+
[/\brk_live_[A-Za-z0-9]{16,}\b/g, 'clave-stripe'],
|
|
25
|
+
|
|
26
|
+
// Formatos estructurados
|
|
27
|
+
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, 'jwt'],
|
|
28
|
+
[/-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g, 'clave-privada'],
|
|
29
|
+
|
|
30
|
+
// Asignaciones con name sensible: TOKEN=..., api_secret: "...", PASSWORD = '...'
|
|
31
|
+
[/\b([A-Za-z_][A-Za-z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|APIKEY|API_KEY|PRIVATE_KEY|CREDENTIAL|ACCESS_KEY)[A-Za-z0-9_]*)\s*[:=]\s*["']?([^\s"',;]{8,})["']?/gi, 'asignacion'],
|
|
32
|
+
|
|
33
|
+
// URL con credenciales embebidas
|
|
34
|
+
[/\b([a-z][a-z0-9+.-]*:\/\/)([^\s:/@]+):([^\s:/@]+)@/gi, 'url-con-credencial'],
|
|
35
|
+
|
|
36
|
+
// Cabecera Authorization
|
|
37
|
+
[/\b(Authorization\s*:\s*(?:Bearer|Basic|token)\s+)([A-Za-z0-9._~+/=-]{12,})/gi, 'cabecera-auth'],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// Patrones de usuario largos no aportan nada que uno corto no haga, y sí
|
|
41
|
+
// alargan el tiempo de cualquier retroceso que la heurística de abajo no
|
|
42
|
+
// atrape. chaski.json se versiona: esto no es para un usuario que se
|
|
43
|
+
// equivoca, es un techo contra un patrón que nadie miró con cuidado.
|
|
44
|
+
const MAX_USER_PATTERN_LEN = 200;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ¿Esta forma de patrón puede producir retroceso catastrófico?
|
|
48
|
+
*
|
|
49
|
+
* Heurística, no un verificador — no existe uno sin dependencias, y escribir
|
|
50
|
+
* uno sería el motor de regex que ya tenemos reimplementado peor. Cubre la
|
|
51
|
+
* forma confirmada en esta máquina: `redact('a'.repeat(35) + '!', { redact:
|
|
52
|
+
* ['(a+)+$'] })` se cuelga indefinidamente (8+ s antes de matarlo a mano).
|
|
53
|
+
* La familia que causa esto es siempre la misma: un grupo que ya repite algo
|
|
54
|
+
* por dentro (`+`, `*` o alternancia `|`) y que además se repite por fuera
|
|
55
|
+
* — `(a+)+`, `(a*)*`, `(a|aa)+`. Eso es lo que se rechaza aquí.
|
|
56
|
+
*
|
|
57
|
+
* Lo que esto NO atrapa, a propósito, porque cubrirlo pide un parser de
|
|
58
|
+
* regex completo y este archivo no lo tiene: cuantificadores anidados a
|
|
59
|
+
* tres niveles (`((a+)+)+`), alternancia superpuesta sin agrupar
|
|
60
|
+
* (`a*a*a*...`), y cualquier patrón que sea lento por razones que no son
|
|
61
|
+
* retroceso exponencial (p. ej. una alternancia con cientos de ramas). El
|
|
62
|
+
* tope de longitud de abajo y el techo de `chaski.json` versionado — un
|
|
63
|
+
* patrón ajeno pasa por code review antes de ejecutarse — son la segunda
|
|
64
|
+
* línea de defensa para lo que se escapa de la heurística.
|
|
65
|
+
*/
|
|
66
|
+
function isCatastrophicShape(source) {
|
|
67
|
+
return /\([^()]*[+*|][^()]*\)(?:[*+]|\{\d+,\})/.test(source);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Patrones extra del usuario, desde chaski.json → redact: ["..."] */
|
|
71
|
+
function userPatterns(config) {
|
|
72
|
+
const extra = [];
|
|
73
|
+
for (const p of (config?.redact ?? [])) {
|
|
74
|
+
if (typeof p !== 'string' || !p || p.length > MAX_USER_PATTERN_LEN) continue;
|
|
75
|
+
if (isCatastrophicShape(p)) {
|
|
76
|
+
process.stderr.write(`chaski: patrón de redact ignorado, puede colgar el hook: ${p}\n`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
try { extra.push([new RegExp(p, 'g'), 'patron-usuario']); } catch {}
|
|
80
|
+
}
|
|
81
|
+
return extra;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Redacta el texto. Devuelve { texto, redactions } — el conteo sirve para
|
|
86
|
+
* que el relevo declare que hubo redactions en lugar de callarlas.
|
|
87
|
+
*/
|
|
88
|
+
export function redact(texto, config = null) {
|
|
89
|
+
if (!texto) return { texto: '', redactions: 0 };
|
|
90
|
+
let out = String(texto);
|
|
91
|
+
let n = 0;
|
|
92
|
+
|
|
93
|
+
for (const [re, etiqueta] of [...PATTERNS, ...userPatterns(config)]) {
|
|
94
|
+
out = out.replace(re, (match, ...grupos) => {
|
|
95
|
+
n++;
|
|
96
|
+
// En asignaciones, URLs y cabeceras se conserva la parte no secreta
|
|
97
|
+
// para que el texto siga siendo legible.
|
|
98
|
+
if (etiqueta === 'asignacion') return `${grupos[0]}=[redactado: secreto]`;
|
|
99
|
+
if (etiqueta === 'url-con-credencial') return `${grupos[0]}${grupos[1]}:[redactado]@`;
|
|
100
|
+
if (etiqueta === 'cabecera-auth') return `${grupos[0]}[redactado]`;
|
|
101
|
+
return `[redactado: ${etiqueta}]`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Cadenas largas de alta entropía que ningún patrón nombró.
|
|
106
|
+
out = out.replace(/\b[A-Za-z0-9+/=_-]{40,}\b/g, (m) => {
|
|
107
|
+
if (!hasHighEntropy(m)) return m;
|
|
108
|
+
n++;
|
|
109
|
+
return '[redactado: cadena-de-alta-entropia]';
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return { texto: out, redactions: n };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Entropía de Shannon por carácter. Un hash o un token supera ~3.5 bits;
|
|
117
|
+
* una palabra o un identificador en código queda por debajo.
|
|
118
|
+
*/
|
|
119
|
+
function hasHighEntropy(s, umbral = 3.6) {
|
|
120
|
+
const freq = Object.create(null);
|
|
121
|
+
for (const c of s) freq[c] = (freq[c] || 0) + 1;
|
|
122
|
+
let h = 0;
|
|
123
|
+
for (const c in freq) {
|
|
124
|
+
const p = freq[c] / s.length;
|
|
125
|
+
h -= p * Math.log2(p);
|
|
126
|
+
}
|
|
127
|
+
return h >= umbral;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** ¿Esta ruta no debe aparecer nunca en el relevo? */
|
|
131
|
+
export function isExcludedPath(ruta, config = null) {
|
|
132
|
+
const defecto = [
|
|
133
|
+
// Lo que chaski mismo escribe: nunca es parte del trabajo del usuario.
|
|
134
|
+
/(^|[\\/])\.tambo([\\/]|$)/i,
|
|
135
|
+
/(^|[\\/])chaski\.local\.json$/i,
|
|
136
|
+
/(^|[\\/])\.env($|\.|[\\/])/i,
|
|
137
|
+
/(^|[\\/])\.?(credentials|secrets?)($|[\\/.])/i,
|
|
138
|
+
/\.(pem|key|pfx|p12|keystore|jks)$/i,
|
|
139
|
+
/(^|[\\/])id_(rsa|dsa|ecdsa|ed25519)($|\.)/i,
|
|
140
|
+
/(^|[\\/])\.npmrc$/i,
|
|
141
|
+
/(^|[\\/])\.aws[\\/]/i,
|
|
142
|
+
];
|
|
143
|
+
const extra = (config?.excludePaths ?? []).map((p) => {
|
|
144
|
+
try { return new RegExp(p, 'i'); } catch { return null; }
|
|
145
|
+
}).filter(Boolean);
|
|
146
|
+
|
|
147
|
+
return [...defecto, ...extra].some((re) => re.test(ruta));
|
|
148
|
+
}
|