iron-edge 2.2.0 → 2.3.1

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.
Files changed (158) hide show
  1. package/README.md +14 -10
  2. package/agents/iron-3d.md +7 -1
  3. package/agents/iron-animations.md +14 -7
  4. package/agents/iron-auditor.md +1 -1
  5. package/agents/iron-brand.md +16 -7
  6. package/agents/iron-ceo.md +2 -1
  7. package/agents/iron-claude.md +16 -9
  8. package/agents/iron-clone.md +5 -2
  9. package/agents/iron-cloning.md +14 -7
  10. package/agents/iron-commercial.md +16 -7
  11. package/agents/iron-community.md +7 -1
  12. package/agents/iron-content.md +14 -7
  13. package/agents/iron-copy.md +16 -7
  14. package/agents/iron-council.md +16 -7
  15. package/agents/iron-courses.md +14 -7
  16. package/agents/iron-cyber.md +14 -7
  17. package/agents/iron-design.md +16 -7
  18. package/agents/iron-eng.md +1 -1
  19. package/agents/iron-finance.md +14 -7
  20. package/agents/iron-forge.md +1 -1
  21. package/agents/iron-growth.md +16 -7
  22. package/agents/iron-install.md +1 -1
  23. package/agents/iron-paidmedia.md +16 -7
  24. package/agents/iron-premium.md +1 -1
  25. package/agents/iron-product.md +16 -7
  26. package/agents/iron-research.md +14 -7
  27. package/agents/iron-search.md +1 -1
  28. package/agents/iron-shopify.md +2 -2
  29. package/agents/iron-spy.md +21 -9
  30. package/agents/iron-storytelling.md +16 -7
  31. package/agents/iron-video.md +7 -1
  32. package/agents/lp-cinematico.md +1 -1
  33. package/agents/xq-advisory.md +20 -3
  34. package/agents/xq-brand.md +19 -2
  35. package/agents/xq-clevel.md +14 -2
  36. package/agents/xq-codecore.md +25 -11
  37. package/agents/xq-copy.md +18 -2
  38. package/agents/xq-copymaster.md +108 -91
  39. package/agents/xq-cyber.md +26 -12
  40. package/agents/xq-data.md +16 -2
  41. package/agents/xq-design.md +24 -8
  42. package/agents/xq-hormozi.md +16 -2
  43. package/agents/xq-movement.md +14 -2
  44. package/agents/xq-storytelling.md +18 -2
  45. package/agents/xq-traffic.md +25 -8
  46. package/bin/iron-edge.js +75 -8
  47. package/installer/gerados.js +38 -2
  48. package/installer/install.js +21 -3
  49. package/installer/update.js +201 -20
  50. package/iron-routing-map.md +19 -2
  51. package/package.json +3 -2
  52. package/skills/blindar-sistema/SKILL.md +123 -0
  53. package/skills/blindar-sistema/advanced/frieren-opcional.md +61 -0
  54. package/skills/blindar-sistema/kb/idor.md +94 -0
  55. package/skills/blindar-sistema/kb/llm-injection.md +86 -0
  56. package/skills/blindar-sistema/kb/secrets.md +89 -0
  57. package/skills/blindar-sistema/kb/sqli.md +77 -0
  58. package/skills/blindar-sistema/procedures/llm-endpoint-hardening.md +80 -0
  59. package/skills/blindar-sistema/procedures/sandbox-dado-falso.md +63 -0
  60. package/skills/blindar-sistema/procedures/secret-hygiene.md +72 -0
  61. package/skills/gate/SKILL.md +6 -0
  62. package/skills/salvar/SKILL.md +21 -1
  63. package/squads/squad-brand/agents/al-ries.md +1 -2
  64. package/squads/squad-brand/agents/alina-wheeler.md +1 -2
  65. package/squads/squad-brand/agents/brand-orqx.md +2 -0
  66. package/squads/squad-brand/agents/byron-sharp.md +1 -2
  67. package/squads/squad-brand/agents/david-aaker.md +1 -2
  68. package/squads/squad-brand/agents/denise-yohn.md +1 -2
  69. package/squads/squad-brand/agents/donald-miller.md +1 -2
  70. package/squads/squad-brand/agents/emily-heyward.md +1 -2
  71. package/squads/squad-brand/agents/jean-noel-kapferer.md +1 -2
  72. package/squads/squad-brand/agents/kevin-keller.md +1 -2
  73. package/squads/squad-brand/agents/marty-neumeier.md +1 -2
  74. package/squads/squad-cloning/agents/mind-synthesizer.md +1 -0
  75. package/squads/squad-cloning/agents/source-hunter.md +1 -0
  76. package/squads/squad-commercial/agents/commercial-orqx.md +2 -0
  77. package/squads/squad-content/agents/signal-intelligence.md +1 -0
  78. package/squads/squad-copy/agents/ad-copywriter.md +1 -0
  79. package/squads/squad-copy/agents/andre-chaperon.md +1 -2
  80. package/squads/squad-copy/agents/ben-settle.md +1 -2
  81. package/squads/squad-copy/agents/claude-hopkins.md +1 -2
  82. package/squads/squad-copy/agents/clayton-makepeace.md +1 -2
  83. package/squads/squad-copy/agents/copy-orqx.md +2 -0
  84. package/squads/squad-copy/agents/dan-kennedy.md +1 -2
  85. package/squads/squad-copy/agents/dan-koe.md +1 -2
  86. package/squads/squad-copy/agents/david-deutsch.md +1 -2
  87. package/squads/squad-copy/agents/david-ogilvy.md +1 -2
  88. package/squads/squad-copy/agents/eugene-schwartz.md +1 -2
  89. package/squads/squad-copy/agents/frank-kern.md +1 -2
  90. package/squads/squad-copy/agents/funnel-copywriter.md +1 -0
  91. package/squads/squad-copy/agents/gary-bencivenga.md +1 -2
  92. package/squads/squad-copy/agents/gary-halbert.md +1 -2
  93. package/squads/squad-copy/agents/jim-rutz.md +1 -2
  94. package/squads/squad-copy/agents/joe-sugarman.md +1 -2
  95. package/squads/squad-copy/agents/john-carlton.md +1 -2
  96. package/squads/squad-copy/agents/jon-benson.md +1 -2
  97. package/squads/squad-copy/agents/parris-lampropoulos.md +1 -2
  98. package/squads/squad-copy/agents/persuasion-psychologist.md +1 -0
  99. package/squads/squad-copy/agents/robert-collier.md +1 -2
  100. package/squads/squad-copy/agents/russell-brunson.md +1 -2
  101. package/squads/squad-copy/agents/ry-schwartz.md +1 -2
  102. package/squads/squad-copy/agents/stefan-georgi.md +1 -2
  103. package/squads/squad-copy/agents/todd-brown.md +1 -2
  104. package/squads/squad-council/agents/brene-brown.md +1 -2
  105. package/squads/squad-council/agents/charlie-munger.md +1 -2
  106. package/squads/squad-council/agents/council-orqx.md +3 -1
  107. package/squads/squad-council/agents/derek-sivers.md +1 -2
  108. package/squads/squad-council/agents/naval-ravikant.md +1 -2
  109. package/squads/squad-council/agents/patrick-lencioni.md +1 -2
  110. package/squads/squad-council/agents/peter-thiel.md +1 -2
  111. package/squads/squad-council/agents/ray-dalio.md +1 -2
  112. package/squads/squad-council/agents/reid-hoffman.md +1 -2
  113. package/squads/squad-council/agents/simon-sinek.md +1 -2
  114. package/squads/squad-council/agents/yvon-chouinard.md +1 -2
  115. package/squads/squad-courses/agents/assessment-creator.md +1 -0
  116. package/squads/squad-cybersecurity/agents/cloud-security-engineer.md +1 -0
  117. package/squads/squad-cybersecurity/agents/compliance-officer.md +1 -0
  118. package/squads/squad-cybersecurity/agents/cyber-orqx.md +1 -0
  119. package/squads/squad-cybersecurity/agents/incident-responder.md +1 -0
  120. package/squads/squad-cybersecurity/agents/network-security-engineer.md +1 -0
  121. package/squads/squad-cybersecurity/agents/penetration-tester.md +1 -0
  122. package/squads/squad-cybersecurity/agents/soc-analyst.md +1 -0
  123. package/squads/squad-cybersecurity/agents/threat-analyst.md +1 -0
  124. package/squads/squad-design/agents/brad-frost.md +4 -3
  125. package/squads/squad-design/agents/dan-mall.md +4 -3
  126. package/squads/squad-design/agents/dave-malouf.md +4 -3
  127. package/squads/squad-design/agents/design-chief.md +2 -0
  128. package/squads/squad-design/agents/design-orqx.md +2 -0
  129. package/squads/squad-design/agents/nano-banana-generator.md +1 -0
  130. package/squads/squad-engineering/agents/eng-devops.md +1 -0
  131. package/squads/squad-engineering/agents/eng-product.md +1 -0
  132. package/squads/squad-engineering/agents/eng-sprint.md +1 -0
  133. package/squads/squad-finance/agents/budget-controller.md +1 -0
  134. package/squads/squad-growth/agents/growth-orqx.md +2 -0
  135. package/squads/squad-paidmedia/agents/depesh-mandalia.md +1 -2
  136. package/squads/squad-paidmedia/agents/kasim-aslam.md +1 -2
  137. package/squads/squad-paidmedia/agents/molly-pittman.md +1 -2
  138. package/squads/squad-paidmedia/agents/nicholas-kusmich.md +1 -2
  139. package/squads/squad-paidmedia/agents/paidmedia-orqx.md +2 -0
  140. package/squads/squad-paidmedia/agents/pedro-sobral.md +1 -2
  141. package/squads/squad-paidmedia/agents/ralph-burns.md +1 -2
  142. package/squads/squad-paidmedia/agents/tom-breeze.md +1 -2
  143. package/squads/squad-product/agents/product-orqx.md +2 -0
  144. package/squads/squad-research/agents/audience-intelligence.md +1 -0
  145. package/squads/squad-research/agents/competitive-intelligence.md +1 -0
  146. package/squads/squad-research/agents/trend-forecaster.md +1 -0
  147. package/squads/squad-storytelling/agents/blake-snyder.md +1 -2
  148. package/squads/squad-storytelling/agents/dan-harmon.md +1 -2
  149. package/squads/squad-storytelling/agents/joseph-campbell.md +1 -2
  150. package/squads/squad-storytelling/agents/keith-johnstone.md +1 -2
  151. package/squads/squad-storytelling/agents/kindra-hall.md +1 -2
  152. package/squads/squad-storytelling/agents/marshall-ganz.md +1 -2
  153. package/squads/squad-storytelling/agents/matthew-dicks.md +1 -2
  154. package/squads/squad-storytelling/agents/nancy-duarte.md +1 -2
  155. package/squads/squad-storytelling/agents/oren-klaff.md +1 -2
  156. package/squads/squad-storytelling/agents/park-howell.md +1 -2
  157. package/squads/squad-storytelling/agents/shawn-coyne.md +1 -2
  158. package/squads/squad-storytelling/agents/storytelling-orqx.md +2 -0
@@ -31,7 +31,12 @@ const DRY = args.includes('--dry-run');
31
31
  const srcIdx = args.indexOf('--source');
32
32
  const SOURCE = srcIdx >= 0 && args[srcIdx + 1] ? path.resolve(args[srcIdx + 1]) : path.join(__dirname, '..');
33
33
 
34
- const log = (m) => console.log(m);
34
+ // Escrita SINCRONA de proposito. Com `console.log` em Windows, quando a saida vai pra um cano
35
+ // (CI, `| tee`, terminal do Claude Code), o Node enfileira e o `process.exit()` do fim descarta o
36
+ // que ainda nao saiu: na CI de 22/09/2026 o log chegou comecando pelo MEIO, sem o cabecalho e sem
37
+ // o "Instalacao encontrada". Um aluno que cola o log pra pedir ajuda mandaria metade dele — e eu
38
+ // perdi duas rodadas de CI deduzindo em cima de um log truncado.
39
+ const log = (m) => { try { fs.writeSync(1, m + '\n'); } catch (_) { console.log(m); } };
35
40
  function abort(msg) {
36
41
  console.error('\n[ABORTADO] ' + msg);
37
42
  process.exit(1);
@@ -50,7 +55,12 @@ function norm(p) {
50
55
  let alvo = path.resolve(p);
51
56
  let resto = '';
52
57
  for (;;) {
53
- try { return path.join(fs.realpathSync(alvo), resto); }
58
+ // `realpathSync.native` (e nao o `realpathSync` puro) porque em Windows so' a versao nativa
59
+ // expande o nome curto 8.3: a mesma pasta aparece como C:\\Users\\RUNNER~1\\... e como
60
+ // C:\\Users\\runneradmin\\..., e comparar as duas como texto faz o update concluir que os 2129
61
+ // arquivos "nao existiam" — e sobrescrever o que a pessoa editou. Medido na CI de 22/09/2026:
62
+ // o manifesto guardava a forma longa e o calculo do momento devolvia a curta.
63
+ try { return path.join(fs.realpathSync.native(alvo), resto); }
54
64
  catch (e) {
55
65
  const pai = path.dirname(alvo);
56
66
  if (pai === alvo) return path.resolve(p);
@@ -111,6 +121,17 @@ for (const f of man.files || []) {
111
121
  if (dest) antigos.set(norm(dest), f);
112
122
  }
113
123
  log(`Instalacao encontrada: ${antigos.size} arquivos no manifesto`);
124
+
125
+ // Chave de diagnostico, ligada por variavel de ambiente. Existe porque em 22/09/2026 o update
126
+ // concluiu, numa maquina Windows, que 2129 arquivos "nao existiam" — com o manifesto certo, o
127
+ // hash certo e o caminho gravado igualzinho ao real. O que diverge e' a forma do caminho DEPOIS
128
+ // do `norm()`, e isso nao da pra ver de fora. Fica no produto de proposito: e' o que permite
129
+ // diagnosticar a maquina de um aluno sem chutar.
130
+ let dbgMostrado = false;
131
+ if (process.env.IRON_EDGE_DEBUG) {
132
+ const amostra = (m) => [...m.keys()].slice(0, 2).join('\n ');
133
+ log(' [debug] chaves do manifesto (antigos):\n ' + amostra(antigos));
134
+ }
114
135
  if (man.label) log(` edicao: ${man.label}`);
115
136
  if (man.installed_at) log(` instalada em: ${man.installed_at}`);
116
137
 
@@ -136,6 +157,7 @@ for (const dir of ['agents', 'skills', 'squads', 'docs']) {
136
157
  const plano = { trocar: [], adicionar: [], restaurar: [], preservar: [], remover: [], manter: [], gerar: [] };
137
158
 
138
159
  for (const [dest, origem] of novos) {
160
+ if (process.env.IRON_EDGE_DEBUG && !dbgMostrado) { dbgMostrado = true; log(' [debug] chave calculada agora (novos):\n ' + dest); }
139
161
  const ent = antigos.get(dest);
140
162
  if (!ent) { plano.adicionar.push({ dest, origem }); continue; }
141
163
  if (ent.generated) { plano.manter.push({ dest, motivo: 'gerado na instalacao (config, manifesto)' }); continue; }
@@ -185,6 +207,59 @@ for (const g of GERADOS) {
185
207
  }
186
208
  }
187
209
 
210
+ // ---------------------------------------------------------------- config: so' relatorio
211
+ // O config NUNCA e' sobrescrito pelo update — e' da pessoa. Mas o que este bloco IMPRIME
212
+ // precisa sair sempre, e por isso ele e' funcao chamada antes do early-exit e nao codigo
213
+ // solto na FASE 3: la' embaixo ele so' rodava quando havia algo pra atualizar. Quem ja' esta'
214
+ // na versao do pacote — que e' justamente quem roda `update` so' pra conferir o cofre —
215
+ // batia no `process.exit(0)` 125 linhas antes e nunca via aviso nenhum. Um alerta contra
216
+ // falha silenciosa que falhava em silencio. Vale para os DOIS avisos daqui: o de campo
217
+ // faltando ja' nascia inalcancavel assim, e ninguem tinha percebido.
218
+ function relatarConfig() {
219
+ const cfg = path.join(IRON_DIR, 'config.yaml');
220
+ if (!fs.existsSync(cfg)) return;
221
+ // .replace de CRLF: o config.yaml e' editado pela PESSOA, e o Bloco de Notas do Windows grava
222
+ // com \r\n. Sem isto, o valor lido da linha vem com um \r invisivel grudado no fim — e o aviso
223
+ // de barra invertida passaria a julgar um caminho que ninguem digitou. Mesma classe do defeito
224
+ // que reprovou a CI de Windows em 22/09/2026.
225
+ const seu = fs.readFileSync(cfg, 'utf8').replace(/\r\n/g, '\n');
226
+ const campos = ['owner', 'businesses', 'vault_path'];
227
+ const faltando = campos.filter((c) => !new RegExp(`^\\s*${c}\\s*:`, 'm').test(seu));
228
+ log(' config.yaml: preservado como estava (nao foi tocado).');
229
+ if (faltando.length) {
230
+ log('');
231
+ log(' ATENCAO: a versao nova usa campos que o seu config nao tem:');
232
+ for (const f of faltando) log(` - ${f}`);
233
+ log(' Acrescente a mao, ou peca ao Iron: "atualiza meu config".');
234
+ if (faltando.includes('vault_path') && /obsidian_vault_path/.test(seu))
235
+ log(' (Voce tem "obsidian_vault_path": e o mesmo cofre. O Iron entende os dois nomes.)');
236
+ }
237
+
238
+ // Barra invertida SOLTA dentro de aspas duplas. O par `\\` e' escape valido e correto — e'
239
+ // o formato que instalacoes antigas gravavam —, entao ele e' consumido ANTES da checagem:
240
+ // avisar nele mandaria a pessoa "consertar" um arquivo que esta' certo. O que sobra e'
241
+ // barra solta, e ela tem dois destinos, os dois ruins: `\U` de C:\Users\<voce> nao e escape
242
+ // valido e derruba o arquivo INTEIRO (o owner morre junto), enquanto `\b` de C:\backup e'
243
+ // escape valido, parseia sem erro nenhum e entrega um caminho CORROMPIDO em silencio.
244
+ // Isola o valor ENTRE ASPAS. Usar a linha inteira fazia comentario a direita entrar na conta:
245
+ // `vault_path: "C:/Users/<voce>/Cofre" # nota com contra\barra` disparava o aviso com o
246
+ // caminho intacto. Aspas duplas nao podem existir dentro de nome de arquivo no Windows, entao
247
+ // [^"]* fecha no lugar certo.
248
+ const linha = (seu.match(/^\s*(?:obsidian_)?vault_path\s*:\s*(.+)$/m) || [])[1];
249
+ const aspas = linha ? linha.trim().match(/^"([^"]*)"/) : null;
250
+ const valor = aspas ? aspas[1] : '';
251
+ if (valor && valor.split('\\\\').join('').includes('\\')) {
252
+ log('');
253
+ log(' ATENCAO: seu vault_path tem barra invertida dentro de aspas duplas:');
254
+ log(' ' + aspas[0]);
255
+ log(' Em YAML isso ou invalida o arquivo INTEIRO (e o owner morre junto), ou entrega um');
256
+ log(' caminho corrompido sem dar erro nenhum. Nos dois casos o sintoma e o mesmo: o Iron');
257
+ log(' passa a responder generico e a oferecer criar um cofre que voce ja tem.');
258
+ log(' Troque as barras invertidas por barras normais (C:/Users/<voce>/...) e salve.');
259
+ log(' Ou peca ao Iron: "conserta meu vault_path".');
260
+ }
261
+ }
262
+
188
263
  // ---------------------------------------------------------------- mostrar
189
264
  log('');
190
265
  log('--- PLANO ---');
@@ -213,8 +288,102 @@ log('');
213
288
  log(' Seu config.yaml, seus agentes e skills proprios, e seu cofre NAO entram neste plano.');
214
289
  log(' Config e manifesto sao regravados no fim; o resto nem e olhado.');
215
290
 
291
+ log('');
292
+ relatarConfig();
293
+
294
+
216
295
  if (DRY) { log(''); log('--dry-run: nada foi escrito.'); process.exit(0); }
217
- if (!plano.trocar.length && !plano.adicionar.length && !plano.restaurar.length && !plano.remover.length && !plano.gerar.length) {
296
+
297
+ // Permissao da pasta de segredos, TAMBEM no update. O chmod entrou no install no ciclo 2 e o
298
+ // gate mediu a lacuna: quem ja tinha o Iron instalado seguia com `secrets/` em 755, porque o
299
+ // caminho de manutencao e' o update, nao reinstalar. Roda DUAS vezes de proposito: aqui, antes
300
+ // do early-exit (senao nao alcanca quem nao tem nada a atualizar) e de novo no fim, porque a
301
+ // FASE 2 pode RECRIAR a pasta com o umask padrao — medido no gate, rodada 4.
302
+ // Idempotente e falha aberto fora de POSIX.
303
+ function fecharPermissoes() {
304
+ for (const [alvo, modo] of [[path.join(IRON_DIR, 'secrets'), 0o700], [path.join(IRON_DIR, 'integrations', 'shopify.yaml'), 0o600]]) {
305
+ try {
306
+ if (!fs.existsSync(alvo)) continue;
307
+ const atual = fs.statSync(alvo).mode & 0o777;
308
+ if (atual === modo) continue;
309
+ fs.chmodSync(alvo, modo);
310
+ log(` permissao de ${path.basename(alvo)} fechada (${atual.toString(8)} -> ${modo.toString(8)})`);
311
+ } catch (_) { /* Windows e sistemas sem POSIX: a ACL do perfil ja cobre */ }
312
+ }
313
+ }
314
+ fecharPermissoes();
315
+
316
+ // metadata.json ilegivel tambem se conserta ANTES do early-exit: o aluno que ja esta na versao do
317
+ // pacote e roda update justamente pra consertar caia em "Nada a fazer" e ficava travado, com o
318
+ // verify reprovando para sempre. Mesmo padrao do chmod, mesmo erro, medido na rodada 4.
319
+ // `somenteSeQuebrado` existe porque a chamada de antes do early-exit nao pode reescrever um
320
+ // metadata SADIO: mudar o arquivo sem regravar o manifesto (que so acontece na FASE 3) deixaria
321
+ // disco e registro divergentes, e o verify reprovaria — o mesmo defeito da rodada 2, por outra porta.
322
+ // Devolve true quando ESCREVEU, e quem chamou usa isso pra nao sair pelo early-exit.
323
+ function repararMetadata(versao, somenteSeQuebrado) {
324
+ const META = path.join(IRON_DIR, 'metadata.json');
325
+ // Arquivo AUSENTE nao e' arquivo quebrado: instalacao antiga simplesmente nao tinha metadata, e
326
+ // inventar um na chamada de ANTES do early-exit transformaria um update no-op em escrita (isso
327
+ // quebrou 8 casos do proprio test-update). Mas na FASE 3, com versao pra gravar, ausente TEM que
328
+ // ser recriado — a rodada 5 mediu que a guarda simples apagava a entrada do manifesto em silencio.
329
+ if (!fs.existsSync(META)) {
330
+ // Sumido MAS registrado no manifesto = o produto sabe que ele deveria existir; entao recriar
331
+ // e' conserto, nao invencao, e vale tambem antes do early-exit: sem isso o verify reprovava
332
+ // com "ausentes: 1" e o update respondia "nada a fazer" — remedio que o proprio produto
333
+ // receita e nao cura, o mesmo padrao do shopify.yaml na rodada 1.
334
+ const registrado = [...antigos.keys()].some((d) => d.endsWith('metadata.json'));
335
+ if (somenteSeQuebrado && !registrado) return false;
336
+ if (!versao) {
337
+ const doMan = (man.version || '').replace(/^iron-edge-/, '');
338
+ if (!doMan) return false;
339
+ versao = doMan; // recria com a versao que o manifesto registra
340
+ }
341
+ try {
342
+ fs.writeFileSync(META, JSON.stringify({ version: 'iron-edge-' + versao, label: 'Iron Edge v' + versao, updated_at: new Date().toISOString(), recriado_por_estar_ausente: true }, null, 2));
343
+ log(' metadata.json estava ausente: recriado');
344
+ return true;
345
+ } catch (e) { log(` aviso: nao consegui criar metadata.json (${e.message})`); return false; }
346
+ }
347
+ let meta = null, precisaReescrever = false, ilegivel = false;
348
+ try { meta = JSON.parse(fs.readFileSync(META, 'utf8')); }
349
+ catch (_) { precisaReescrever = true; ilegivel = true; }
350
+ // JSON valido que NAO e' objeto plano (array, numero, texto, null) engole a atribuicao sem
351
+ // erro e o arquivo fica com lixo enquanto o log diz "atualizado" — medido na rodada 4.
352
+ if (!precisaReescrever && (meta === null || typeof meta !== 'object' || Array.isArray(meta))) { precisaReescrever = true; ilegivel = true; }
353
+ // Conteudo que NAO bate com o hash registrado tambem e' reparo: o metadata.json e' derivado,
354
+ // ninguem deveria edita-lo, e quando alguem edita o verify reprova para sempre sem caminho de
355
+ // volta (medido na matriz de estados: "metadata com campo extra do aluno").
356
+ if (!precisaReescrever) {
357
+ const ent = [...antigos.values()].find((e) => String(e.dest).endsWith('metadata.json'));
358
+ if (ent && ent.sha256 && ent.sha256 !== sha256(META)) precisaReescrever = true;
359
+ }
360
+
361
+ if (!precisaReescrever && (somenteSeQuebrado || !versao)) return false;
362
+ // Reparo por DIVERGENCIA (o arquivo esta legivel, so' nao bate com o registro) preserva o que
363
+ // estiver la: quem escreveu algo dentro nao pode perder isso porque o hash saiu de sincronia.
364
+ // Reescrita do zero fica so' para o arquivo ilegivel, onde nao ha o que preservar.
365
+ const base = ilegivel ? { recuperado_de_arquivo_ilegivel: true } : (meta || {});
366
+ if (versao) { base.version = 'iron-edge-' + versao; base.label = 'Iron Edge v' + versao; }
367
+ base.updated_at = new Date().toISOString();
368
+ try {
369
+ fs.writeFileSync(META, JSON.stringify(base, null, 2));
370
+ log(ilegivel ? ' metadata.json estava ilegivel: reescrito do zero'
371
+ : precisaReescrever ? ` metadata.json estava fora de sincronia: reconciliado (${base.version})`
372
+ : ` metadata.json atualizado para ${base.version}`);
373
+ return true;
374
+ } catch (e) { log(` aviso: nao consegui gravar metadata.json (${e.message})`); return false; }
375
+ }
376
+ // so conserta o que esta quebrado; se consertou, o update NAO pode sair pelo early-exit, porque
377
+ // o manifesto precisa da FASE 3 pra registrar o hash novo.
378
+ const metadataReparado = repararMetadata(process.env.IRON_EDGE_VERSION || '', true);
379
+
380
+ // "Nada a fazer" so' vale se a VERSAO tambem ja for a deste pacote. Release que nao mexe em
381
+ // nenhum arquivo rastreado (so' installer/, scripts/, README) caia aqui e a versao registrada
382
+ // ficava velha para sempre — e a mensagem dizia justamente que ja estava na versao do pacote.
383
+ const versaoRegistrada = man.version || '';
384
+ const versaoDoPacote = process.env.IRON_EDGE_VERSION ? 'iron-edge-' + process.env.IRON_EDGE_VERSION : '';
385
+ const versaoDesatualizada = Boolean(versaoDoPacote) && versaoRegistrada !== versaoDoPacote;
386
+ if (!plano.trocar.length && !plano.adicionar.length && !plano.restaurar.length && !plano.remover.length && !plano.gerar.length && !plano.preservar.length && !versaoDesatualizada && !metadataReparado) {
218
387
  log(''); log('Nada a fazer: sua instalacao ja esta na versao deste pacote.');
219
388
  process.exit(0);
220
389
  }
@@ -274,6 +443,26 @@ let geradosOk = 0;
274
443
  for (const g of plano.gerar) {
275
444
  try {
276
445
  fs.mkdirSync(path.dirname(g.dest), { recursive: true });
446
+ // Achado tardio da rodada 4: `~/.claude/commands/` e' pasta generica do Claude Code, e um
447
+ // arquivo com o mesmo nome que NAO esteja no manifesto era sobrescrito sem aviso e sem
448
+ // backup — perda de dado silenciosa do que era do usuario. Agora vira .iron-old antes.
449
+ if (fs.existsSync(g.dest)) {
450
+ const registrado = antigos.has(norm(g.dest));
451
+ if (!registrado) {
452
+ // .iron-old ja existente pode ser backup ANTERIOR do usuario, ou sobra de execucao
453
+ // interrompida: sobrescrever aqui seria repetir, dentro do mecanismo de backup, a perda
454
+ // de dado que ele foi criado pra evitar (medido na rodada 5). Entao abre um nome novo.
455
+ let backup = g.dest + '.iron-old';
456
+ if (fs.existsSync(backup)) {
457
+ const carimbo = new Date().toISOString().replace(/[:.]/g, '-');
458
+ backup = `${g.dest}.iron-old-${carimbo}`;
459
+ }
460
+ try {
461
+ fs.copyFileSync(g.dest, backup);
462
+ log(` ${path.basename(g.dest)} existia e nao era do pacote: copia guardada em ${path.basename(backup)}`);
463
+ } catch (e2) { log(` aviso: nao consegui guardar copia de ${g.dest} (${e2.message}) — nao sobrescrevi`); continue; }
464
+ }
465
+ }
277
466
  fs.writeFileSync(g.dest, g.conteudo);
278
467
  geradosOk++;
279
468
  } catch (e) { log(` aviso: nao consegui gravar ${g.dest} (${e.code || e.message})`); }
@@ -284,6 +473,14 @@ if (geradosOk) log(` ${geradosOk} arquivo(s) gerado(s) desta versao criados (ex
284
473
  log('');
285
474
  log('--- FASE 3: manifesto e config ---');
286
475
 
476
+
477
+ // A VERSAO PRIMEIRO, o manifesto DEPOIS. Na rodada 2 do gate o metadata.json era reescrito no
478
+ // fim, depois de o manifesto ja ter guardado o hash do conteudo ANTIGO: disco e registro
479
+ // discordavam e o verify passava a reprovar uma instalacao sadia para sempre.
480
+ const versaoNova = process.env.IRON_EDGE_VERSION || '';
481
+ repararMetadata(versaoNova);
482
+ fecharPermissoes(); // a FASE 2 pode ter recriado secrets/ com o umask padrao
483
+
287
484
  const arquivos = [];
288
485
  const editados = new Set(plano.preservar.map((x) => norm(x.dest)));
289
486
  for (const [dest, origem] of novos) {
@@ -310,6 +507,7 @@ for (const g of GERADOS) {
310
507
 
311
508
  const novoMan = {
312
509
  ...man,
510
+ ...(versaoNova ? { version: 'iron-edge-' + versaoNova, label: 'Iron Edge v' + versaoNova } : {}),
313
511
  updated_at: new Date().toISOString(),
314
512
  previous_installed_at: man.installed_at,
315
513
  files: arquivos,
@@ -317,23 +515,6 @@ const novoMan = {
317
515
  fs.writeFileSync(MANIFEST, JSON.stringify(novoMan, null, 2));
318
516
  log(` manifesto regravado: ${arquivos.length} arquivos`);
319
517
 
320
- // config: NUNCA sobrescrito. Mas se a versao nova tem campo que o seu nao tem, avisa.
321
- const cfg = path.join(IRON_DIR, 'config.yaml');
322
- if (fs.existsSync(cfg)) {
323
- const seu = fs.readFileSync(cfg, 'utf8');
324
- const campos = ['owner', 'businesses', 'vault_path'];
325
- const faltando = campos.filter((c) => !new RegExp(`^\\s*${c}\\s*:`, 'm').test(seu));
326
- log(' config.yaml: preservado como estava (nao foi tocado).');
327
- if (faltando.length) {
328
- log('');
329
- log(' ATENCAO: a versao nova usa campos que o seu config nao tem:');
330
- for (const f of faltando) log(` - ${f}`);
331
- log(' Acrescente a mao, ou peca ao Iron: "atualiza meu config".');
332
- if (faltando.includes('vault_path') && /obsidian_vault_path/.test(seu))
333
- log(' (Voce tem "obsidian_vault_path": e o mesmo cofre. O Iron entende os dois nomes.)');
334
- }
335
- }
336
-
337
518
  log('');
338
519
  log('================================');
339
520
  log(' Atualizado com sucesso');
@@ -63,10 +63,14 @@ o iron-ceo oferece: iron-X (rapido) ou xq-X (profundo)? [r/p]
63
63
  | iron-forge | Construtor de agentes | criar agente, criar skill, novo agente, construir ferramenta |
64
64
  | iron-install | Instalacao | instalar, setup do ecossistema, configurar ambiente |
65
65
  | iron-claude | Claude Code em si | hook, MCP, skill, subagente, settings.json, contexto, memoria, "o Claude esta estranho", auditoria da instalacao |
66
- | iron-clone | Clonar SITE + design system | clonar site, copiar pagina, espelhar site, "aquele site com a minha marca", DNA visual de referencia, extrair paleta/tipografia, transplante de marca, organizar HTML monolitico (NAO confundir com iron-cloning, que clona o MODO DE PENSAR de uma pessoa real). ATENCAO: a skill clone-site nao roda nativamente em Windows — avisar antes |
66
+ | iron-clone | Clonar SITE + design system | clonar site, copiar pagina, espelhar site, DNA visual de referencia, extrair paleta, transplante de marca, organizar HTML |
67
67
  | iron-eng | Engenharia de software | app, plataforma, sistema, API, backend, banco, login, painel, dashboard, CRM, integracao, automacao de negocio, script, job, bug, erro, quebrou, deploy, migration |
68
68
  | lp-cinematico | Landing pages scroll | landing page, LP cinematografica, scroll-scrub, pagina de captura |
69
69
 
70
+ > **iron-clone x iron-cloning:** o `iron-clone` copia SITE e identidade visual; o `iron-cloning` copia o MODO DE PENSAR
71
+ > de uma pessoa real (e exige consentimento e base legal). A skill `clone-site` nao roda nativamente em Windows, e o
72
+ > proprio `iron-clone` avisa isso antes de comecar.
73
+
70
74
  ## 3. Routing Table — Camada de Aprofundamento (XQ — 13)
71
75
 
72
76
  | Agente | Sobe da camada | Quando usar |
@@ -77,7 +81,7 @@ o iron-ceo oferece: iron-X (rapido) ou xq-X (profundo)? [r/p]
77
81
  | xq-codecore | squad-claude | maestria em Claude Code |
78
82
  | xq-copy | iron-copy | copy com mestres (Halbert, Schwartz, Ogilvy) |
79
83
  | xq-copymaster | iron-copy | copy expandida — 33 copywriters |
80
- | xq-cyber | iron-cyber | seguranca com 15 especialistas |
84
+ | xq-cyber | iron-cyber | seguranca em profundidade (7 especialistas instalados) |
81
85
  | xq-data | iron-growth | analytics e metricas em profundidade |
82
86
  | xq-design | iron-design | UX/UI com experts |
83
87
  | xq-hormozi | iron-commercial | ofertas estilo Grand Slam Offers |
@@ -274,3 +278,16 @@ pacote e nao entram na Routing Table.
274
278
  Regra: o iron-ceo NAO aciona agente que nao esteja na Routing Table num
275
279
  fluxo normal — so quando voce pedir pelo nome. Isso evita que um agente de
276
280
  projeto especifico seja escolhido para uma demanda generica.
281
+
282
+ ## 8. Especialistas com nome de pessoa real
283
+
284
+ Arquivos de especialista marcados com `real_person: true` levam o nome de uma pessoa que existe. A regra, sem exceção, para qualquer agente que assuma um deles:
285
+
286
+ 1. O agente aplica o MÉTODO público da pessoa (frameworks, princípios, obras publicadas). Ele não é a pessoa e não diz que é. Forma correta: "no método de Gary Halbert". Forma errada: "eu sou Gary Halbert".
287
+ 2. Nunca atribuir à pessoa frase, opinião, endosso ou fato que não esteja em obra publicada dela. O que for dedução do modelo se declara como dedução: "pelo método dele, eu diria que...".
288
+ 3. O entregável nunca sai assinado com o nome da pessoa, nem sugere que ela revisou, aprovou ou participou.
289
+ 4. Se o pedido for conteúdo que finja ser da pessoa (post "escrito por", depoimento, citação inventada), o agente recusa essa parte e entrega a versão "no método de".
290
+
291
+ 5. **Pessoa falecida** (o arquivo registra a morte, seja num campo `died:` seja dentro do `born:`, como "1926 (died 2022)") exige o cuidado maior, não o menor: ela não pode desmentir nada, e o espólio costuma manter direito de imagem. Nesses casos, cite a obra publicada de forma literal e atribuída, e nunca produza frase nova no nome dela.
292
+
293
+ Esta seção é a fonte única desta regra. Quem dá entrada nesses especialistas aponta pra cá; não existe cópia da regra nos arquivos de especialista.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iron-edge",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "description": "Ecossistema de agentes para Claude Code: 44 agentes de topo, 18 squads e 14 skills, com quality gate, memoria e instalador que nunca sobrescreve.",
5
5
  "keywords": [
6
6
  "claude",
@@ -31,7 +31,8 @@
31
31
  "node": ">=18.0.0"
32
32
  },
33
33
  "scripts": {
34
- "test": "node scripts/lint.js"
34
+ "test": "node scripts/lint.js && node scripts/test-verify.js && node scripts/test-update.js",
35
+ "test:matriz": "node scripts/test-matriz-update.js"
35
36
  },
36
37
  "devDependencies": {
37
38
  "js-yaml": "^5.3.0"
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: blindar-sistema
3
+ description: >
4
+ Auditoria de segurança guiada para quem programa com IA (vibe coding) e não
5
+ domina segurança. O Iron LÊ o código do próprio sistema do usuário (white-box):
6
+ acha as portas abertas mais comuns sem proxy, sem certificado, sem instalar nada,
7
+ sem mandar dado para fora, e roda em Mac e Windows. Traduz cada achado em
8
+ linguagem simples com prioridade e conduz o conserto. Use quando o usuário disser
9
+ "blindar meu sistema", "auditar a segurança do meu app/site", "revisar segurança",
10
+ "meu sistema está seguro?", ou pedir revisão de um sistema feito com IA.
11
+ ---
12
+
13
+ # blindar-sistema
14
+
15
+ Você é um copiloto de segurança para quem programa com IA e não entende de
16
+ segurança. Fale simples, sem jargão. Explique cada termo técnico na hora, em uma
17
+ linha. Nunca assuste sem dar o conserto. Uma coisa de cada vez.
18
+
19
+ Você audita LENDO o código do sistema do próprio usuário. Você não ataca nada, não
20
+ liga proxy, não instala ferramenta, não manda dado para fora. Todo achado sai da
21
+ leitura do repositório.
22
+
23
+ ## Regra dura
24
+
25
+ Só audite um sistema que é do próprio usuário. Você só LÊ o código, isso é seguro
26
+ em qualquer ambiente. Se em algum momento o usuário quiser o teste ao vivo (a etapa
27
+ avançada, com a ferramenta externa Frieren), aí vale a trava: só contra sistema
28
+ dele, só em ambiente de teste com dado sintético, nunca produção, nunca dado de
29
+ gente real, nunca site de terceiro. Veja `advanced/frieren-opcional.md`.
30
+
31
+ ## A base de conhecimento
32
+
33
+ Cada família de falha tem um arquivo em `kb/` que diz onde procurar no código, o
34
+ padrão vulnerável vs. o seguro, como confirmar e o conserto. LEIA o arquivo da
35
+ família antes de auditar aquela família, não audite de memória.
36
+
37
+ - `kb/idor.md`: um cliente vê o dado de outro
38
+ - `kb/secrets.md`: chave/segredo exposto (front, histórico do Git, resposta da API)
39
+ - `kb/sqli.md`: injeção de SQL
40
+ - `kb/llm-injection.md`: injeção de prompt no endpoint de IA
41
+
42
+ Procedimentos de conserto que o usuário faz junto:
43
+ - `procedures/secret-hygiene.md`: revogar, rotacionar e limpar segredo do Git
44
+ - `procedures/llm-endpoint-hardening.md`: blindar o próprio LLM do sistema
45
+ - `procedures/sandbox-dado-falso.md`: montar um ambiente de teste com dado
46
+ sintético ANTES de qualquer prova viva ou da etapa avançada (Frieren)
47
+
48
+ ## Fluxo
49
+
50
+ ### Fase 0: Medir o sistema (antes de qualquer coisa)
51
+
52
+ NÃO assuma arquivo, rota ou botão que não viu. Meça primeiro. Pergunte uma de cada
53
+ vez o que não der para descobrir lendo:
54
+
55
+ 1. Onde fica o código do sistema (a pasta do repositório)?
56
+ 2. Que stack é (linguagem/framework)? Descubra lendo: `package.json`, arquivos de
57
+ config, estrutura de pastas. Só pergunte se não der para ler.
58
+ 3. O sistema usa IA/LLM por trás (chatbot, geração de texto, agente)?
59
+ 4. Usa banco? Qual (Supabase, Postgres, Mongo)?
60
+
61
+ Depois, MEÇA o terreno lendo o repo, e anote para si:
62
+ - Onde estão os handlers de rota (onde o servidor responde pedidos).
63
+ - Onde está a autenticação (login, sessão, middleware).
64
+ - Se usa Supabase: quais tabelas existem e quais têm RLS ligada.
65
+ - Onde texto externo entra no prompt do modelo (se usa IA).
66
+ - Onde poderia haver segredo (env, config, histórico do Git).
67
+
68
+ Se não houver como ler o repo (o usuário não apontou a pasta), pare e peça a pasta.
69
+ Não invente a estrutura.
70
+
71
+ ### Fase 1: Auditar família por família, LENDO o código
72
+
73
+ Para cada família, abra o `kb/` dela e siga o "onde procurar" e o "como confirmar".
74
+ Ordem sugerida (as portas mais comuns primeiro): IDOR, segredos, SQL, injeção de
75
+ prompt (se usa IA). Depois, se der tempo, as demais famílias conforme forem adicionadas.
76
+
77
+ Para cada achado, registre: arquivo e linha, o que é (uma frase de leigo), o risco
78
+ concreto para o negócio dele, e o conserto em passos curtos no framework dele.
79
+
80
+ ### Fase 2: Traduzir e priorizar
81
+
82
+ Não despeje a lista inteira. Ordene por gravidade e comece pelos 3 mais graves.
83
+
84
+ Crítico é qualquer coisa que exponha dado de cliente, segredo, ou dê acesso sem
85
+ login. Para cada achado dos 3 primeiros:
86
+ - Uma frase do que é, em linguagem de leigo.
87
+ - O risco concreto (o que um estranho consegue fazer).
88
+ - O conserto em passos curtos, com exemplo no framework dele.
89
+
90
+ ### Fase 3: Plano curto
91
+
92
+ Monte: o que consertar hoje (crítico), esta semana, depois. Uma linha cada.
93
+
94
+ ### Fase 4: As correções que exigem procedimento
95
+
96
+ Quando aparecer segredo no Git ou endpoint de IA sem proteção, conduza pelo
97
+ `procedures/`. São as duas correções que pedem passo a passo (revogar/limpar
98
+ segredo; blindar o LLM). Uma etapa de cada vez, esperando o usuário terminar.
99
+
100
+ ### Fase 5: Fechar
101
+
102
+ Entregue um checklist do que foi corrigido e do que ficou de tarefa. Lembre que,
103
+ depois de consertar, vale reauditar: a correção às vezes cria o defeito seguinte.
104
+
105
+ ## Se der errado
106
+
107
+ - **Não acho o código / a pasta está vazia:** peça o caminho certo do repositório.
108
+ Não audite pela tela pública; sem o código, a auditoria é chute.
109
+ - **O repo é enorme e trava a leitura:** comece pelos handlers de rota e pela pasta
110
+ de API; é onde as portas abertas moram. Não precisa ler tudo de uma vez.
111
+ - **Não tenho certeza se um achado é real:** trate como hipótese, marque "a
112
+ confirmar" e diga ao usuário como provar no sandbox de teste. Nunca afirme falha
113
+ sem a linha do código que a prova.
114
+ - **Windows:** os comandos de varredura dos `kb/` usam `git grep`, que roda igual no
115
+ Mac e no Windows (é o próprio Git). Onde um `kb/` traz um pipe `git log -p | grep`,
116
+ ele dá também a versão PowerShell com `Select-String`. Nada de encadear com `&&`
117
+ (o PowerShell 5.1 não aceita); uma linha por comando.
118
+
119
+ ## Tom
120
+
121
+ Vibe coder não tem culpa de não saber isto. Trate como quem constrói rápido e
122
+ precisa de um segundo par de olhos, não como quem errou. Elogie o que já está certo.
123
+ Sempre técnico mais tradução fácil, lado a lado. Uma coisa de cada vez.
@@ -0,0 +1,61 @@
1
+ # Etapa avançada (opcional): teste ao vivo com o Frieren
2
+
3
+ > Parte do pacote **blindar-sistema**. Isto é OPCIONAL e AVANÇADO. A trilha
4
+ > principal (a skill lendo o código) NÃO depende disto. Só siga aqui se você quer o
5
+ > teste ao vivo e já montou o sandbox de dado falso (`procedures/sandbox-dado-falso.md`).
6
+
7
+ ## Por que existe
8
+
9
+ A auditoria lendo o código pega quase tudo, e sem risco. Mas algumas falhas só
10
+ aparecem com o sistema RODANDO: injeção cega por tempo, comportamento de cache,
11
+ fluxo de várias etapas (carrinho, cupom, checkout), upload de arquivo. Para esses
12
+ casos existe uma ferramenta externa aberta, o **Frieren DAST-AI** (KnowBe4, licença
13
+ Apache 2.0), que testa o sistema por dentro como um invasor faria.
14
+
15
+ Ela cobre o que a leitura de código não vê, ao custo de 4 riscos que você precisa
16
+ conhecer ANTES de ligar.
17
+
18
+ ## Os 4 riscos (nomeados, para você decidir com os olhos abertos)
19
+
20
+ 1. **O comando de instalação roda código de terceiro.** O `make setup` executa um
21
+ arquivo que vem do download. Rode só numa máquina onde isso é aceitável.
22
+ 2. **Instalar o certificado do proxy é instalar uma autoridade.** O Frieren fica no
23
+ meio e descriptografa o tráfego seguro. Enquanto o certificado estiver confiado
24
+ no navegador, TODO o seu tráfego pode ser lido, não só o sistema em teste. Use um
25
+ navegador ou perfil separado, só para o teste, e remova o certificado no fim.
26
+ 3. **O tráfego capturado vai para uma LLM.** Os agentes mandam o que capturaram para
27
+ o provedor de IA, na sua chave. Rodar contra dado real é dado saindo da máquina.
28
+ Por isso: SÓ contra o sandbox de dado falso.
29
+ 4. **A ferramenta é nova.** Projeto recente, pouca revisão da comunidade. A marca por
30
+ trás é real; o código ainda é jovem. Trate como ferramenta de laboratório.
31
+
32
+ ## A trava (herda a regra única do sandbox, endurecida)
33
+
34
+ > Só rode o Frieren contra um sistema **seu**, no **sandbox de dado falso** da
35
+ > `procedures/sandbox-dado-falso.md`. Nunca produção. Nunca dado de gente real.
36
+ > Nunca site de terceiro. Confirme os itens do checklist do sandbox antes de ligar.
37
+
38
+ ## Como rodar (resumo, o guia oficial está no repositório da ferramenta)
39
+
40
+ 1. Monte o sandbox de dado falso (procedimento próprio) e confirme o checklist.
41
+ 2. Baixe e instale o Frieren seguindo o README oficial do projeto (repositório
42
+ público da KnowBe4). Use uma chave de LLM sua.
43
+ 3. Suba o proxy, configure o navegador de teste para passar por ele, instale o
44
+ certificado SÓ nesse navegador/perfil.
45
+ 4. Navegue pelo seu sistema no sandbox, logado. Os achados aparecem sozinhos.
46
+ 5. Leia os achados e leve cada CONSERTO para o código (use os `kb/` do pacote para
47
+ traduzir e consertar). O conserto vai para produção; o teste, nunca.
48
+
49
+ ## Depois
50
+
51
+ - Remova o certificado do navegador de teste.
52
+ - Desligue o proxy; não deixe o sandbox exposto com o proxy no ar.
53
+ - Reaudite lendo o código depois de consertar (a correção às vezes cria o defeito
54
+ seguinte).
55
+
56
+ ## O que o Iron já faz sem isto
57
+
58
+ IDOR, segredo exposto, injeção de SQL, injeção de prompt e as demais famílias do
59
+ `kb/`: tudo isso a skill acha LENDO o código, sem proxy, sem certificado, sem mandar
60
+ dado para fora. Esta etapa é só para o teste ao vivo do que só existe em tempo de
61
+ execução.
@@ -0,0 +1,94 @@
1
+ # IDOR: um cliente vê o dado de outro
2
+
3
+ > Base de conhecimento do pacote **blindar-sistema**. Auditoria white-box: o Iron
4
+ > LÊ o código, não ataca de fora. Nenhum dado de cliente sai da máquina.
5
+
6
+ ## O que é (linguagem de leigo)
7
+
8
+ IDOR (Insecure Direct Object Reference) é quando o sistema entrega um recurso pelo
9
+ seu identificador sem conferir se ele pertence a quem está pedindo. O usuário troca
10
+ `pedido/1042` por `pedido/1043` e vê o pedido do vizinho: nome, endereço, o que
11
+ comprou. O sistema entregou o dado sozinho, ninguém precisou invadir.
12
+
13
+ É a falha número 1 do OWASP (Broken Access Control) e a mais comum em app de vibe
14
+ coding, porque a IA constrói a rota que busca pelo ID e quase nunca adiciona sozinha
15
+ a checagem "esse ID é desta pessoa?".
16
+
17
+ ## Onde procurar no código
18
+
19
+ O Iron abre os **handlers de rota** (onde o servidor responde um pedido) e procura
20
+ todo ponto que:
21
+ 1. Recebe um identificador vindo de fora: `params`, query string, corpo do pedido,
22
+ segmento da URL (`/pedido/:id`), ou variável de GraphQL.
23
+ 2. Usa esse identificador para buscar no banco (`select ... where id = :id`,
24
+ `findById`, `.get(id)`, `from('pedidos').eq('id', id)`).
25
+ 3. **Não filtra também pelo dono** logado (`user_id`, `owner_id`, `account_id`).
26
+
27
+ Alvos por stack (onde o handler mora):
28
+ - **Next.js / API routes:** `app/api/**/route.ts`, `pages/api/**`.
29
+ - **Supabase:** a checagem de dono deve estar na **RLS (Row Level Security)** da
30
+ tabela, não só na query. Tabela sem RLS é IDOR aberto por padrão.
31
+ - **Express / Node:** arquivos de `routes/` e `controllers/`.
32
+ - **GraphQL:** cada **resolver** que recebe um `id` como argumento.
33
+
34
+ ## Padrão vulnerável vs. seguro
35
+
36
+ VULNERÁVEL, busca só pelo ID:
37
+ ```
38
+ // Express: confia no ID e devolve
39
+ const pedido = await db.pedido.findUnique({ where: { id: req.params.id } })
40
+ res.json(pedido)
41
+ ```
42
+ ```
43
+ // Supabase sem RLS: isto retorna QUALQUER pedido
44
+ const { data } = await supabase.from('pedidos').select('*').eq('id', id)
45
+ ```
46
+
47
+ SEGURO, o dono entra sempre na condição, no servidor:
48
+ ```
49
+ // Express: o pedido só é achado se for do usuário logado
50
+ const pedido = await db.pedido.findFirst({
51
+ where: { id: req.params.id, userId: req.user.id }
52
+ })
53
+ if (!pedido) return res.status(404).end() // 404, não 403: não revela que existe
54
+ ```
55
+ ```sql
56
+ -- Supabase: a trava vive no banco, vale para toda query
57
+ alter table pedidos enable row level security;
58
+ create policy "dono le o proprio pedido"
59
+ on pedidos for select
60
+ using (auth.uid() = user_id);
61
+ ```
62
+
63
+ ## Caso mais perigoso: escalada horizontal entre contas
64
+
65
+ Não é só trocar o número. Às vezes a rota confere o dono, mas **uma sessão consegue
66
+ usar o recurso de outra**. Teste white-box: existe alguma rota que muda estado
67
+ (cancelar, aprovar, transferir) e checa só se está logado, não se é o dono daquele
68
+ recurso específico? Isso é a categoria BOLA (acesso a objeto de outro usuário); a
69
+ variante em que a falha está numa FUNÇÃO administrativa se chama BFLA. As duas somem
70
+ no meio de painéis admin.
71
+
72
+ ## Como confirmar (sem atacar produção)
73
+
74
+ 1. Liste as rotas que recebem um ID de fora (a Fase 0 da skill já mapeou).
75
+ 2. Para cada uma, responda por escrito: **"o dono do recurso é conferido no
76
+ servidor, nesta query?"** Se a resposta for "a tela só mostra o botão para o
77
+ dono", está decorativo, não protege. Quem chama a rota direto ignora a tela.
78
+ 3. Em Supabase, liste as tabelas do schema público e cruze com as que têm policy:
79
+ `select tablename from pg_tables where schemaname = 'public'`. Tabela com dado de
80
+ cliente e sem RLS é achado crítico.
81
+ 4. Prova viva (só no sandbox de dado falso, Fase 4): crie dois usuários de teste,
82
+ pegue um ID do usuário A e tente lê-lo logado como B. Abriu, é IDOR.
83
+
84
+ ## O conserto
85
+
86
+ - A autorização vive **no servidor e/ou no banco**, nunca na interface.
87
+ - Toda query de recurso de usuário carrega o dono na condição (`where dono = logado`).
88
+ - Em Supabase, **RLS ligada em toda tabela com dado de gente**, com policy por
89
+ operação (select/insert/update/delete).
90
+ - Recurso não encontrado responde **404**, não 403: 403 confirma que o ID existe.
91
+ - IDs sequenciais (1, 2, 3) facilitam a varredura; UUID não conserta o IDOR, mas
92
+ para a enumeração em massa. Corrija a autorização primeiro, troque para UUID depois.
93
+
94
+ **Severidade:** crítico quando expõe dado de cliente, segredo ou dá acesso sem login.