@pathbit/agentdevkit 0.0.9 → 0.0.11

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 (122) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +6 -0
  2. package/.pbai4devs/INSTRUCTIONS.md +3 -0
  3. package/.pbai4devs/agents/README.md +19 -0
  4. package/.pbai4devs/agents/auditor-seguranca/AGENT.md +20 -0
  5. package/.pbai4devs/agents/auditor-seguranca/antigravity.yaml +8 -0
  6. package/.pbai4devs/agents/auditor-seguranca/claude.yaml +5 -0
  7. package/.pbai4devs/agents/auditor-seguranca/cursor.yaml +5 -0
  8. package/.pbai4devs/agents/auditor-seguranca/gemini.yaml +6 -0
  9. package/.pbai4devs/agents/auditor-seguranca/github.yaml +5 -0
  10. package/.pbai4devs/agents/auditor-seguranca/kimi-code.yaml +5 -0
  11. package/.pbai4devs/agents/auditor-seguranca/mimocode.yaml +8 -0
  12. package/.pbai4devs/agents/auditor-seguranca/opencode.yaml +11 -0
  13. package/.pbai4devs/agents/documentador/AGENT.md +19 -0
  14. package/.pbai4devs/agents/documentador/antigravity.yaml +8 -0
  15. package/.pbai4devs/agents/documentador/claude.yaml +5 -0
  16. package/.pbai4devs/agents/documentador/cursor.yaml +5 -0
  17. package/.pbai4devs/agents/documentador/gemini.yaml +6 -0
  18. package/.pbai4devs/agents/documentador/github.yaml +5 -0
  19. package/.pbai4devs/agents/documentador/kimi-code.yaml +5 -0
  20. package/.pbai4devs/agents/documentador/mimocode.yaml +8 -0
  21. package/.pbai4devs/agents/documentador/opencode.yaml +11 -0
  22. package/.pbai4devs/agents/implementador/AGENT.md +20 -0
  23. package/.pbai4devs/agents/implementador/antigravity.yaml +8 -0
  24. package/.pbai4devs/agents/implementador/claude.yaml +5 -0
  25. package/.pbai4devs/agents/implementador/cursor.yaml +5 -0
  26. package/.pbai4devs/agents/implementador/gemini.yaml +6 -0
  27. package/.pbai4devs/agents/implementador/github.yaml +5 -0
  28. package/.pbai4devs/agents/implementador/kimi-code.yaml +5 -0
  29. package/.pbai4devs/agents/implementador/mimocode.yaml +8 -0
  30. package/.pbai4devs/agents/implementador/opencode.yaml +11 -0
  31. package/.pbai4devs/agents/planejador/AGENT.md +19 -0
  32. package/.pbai4devs/agents/planejador/antigravity.yaml +8 -0
  33. package/.pbai4devs/agents/planejador/claude.yaml +5 -0
  34. package/.pbai4devs/agents/planejador/cursor.yaml +5 -0
  35. package/.pbai4devs/agents/planejador/gemini.yaml +6 -0
  36. package/.pbai4devs/agents/planejador/github.yaml +5 -0
  37. package/.pbai4devs/agents/planejador/kimi-code.yaml +5 -0
  38. package/.pbai4devs/agents/planejador/mimocode.yaml +8 -0
  39. package/.pbai4devs/agents/planejador/opencode.yaml +11 -0
  40. package/.pbai4devs/agents/validador/AGENT.md +19 -0
  41. package/.pbai4devs/agents/validador/antigravity.yaml +8 -0
  42. package/.pbai4devs/agents/validador/claude.yaml +5 -0
  43. package/.pbai4devs/agents/validador/cursor.yaml +5 -0
  44. package/.pbai4devs/agents/validador/gemini.yaml +6 -0
  45. package/.pbai4devs/agents/validador/github.yaml +5 -0
  46. package/.pbai4devs/agents/validador/kimi-code.yaml +5 -0
  47. package/.pbai4devs/agents/validador/mimocode.yaml +8 -0
  48. package/.pbai4devs/agents/validador/opencode.yaml +11 -0
  49. package/.pbai4devs/commands/README.md +13 -0
  50. package/.pbai4devs/commands/atualizar-documentacao/COMMAND.md +17 -0
  51. package/.pbai4devs/commands/auditar-seguranca/COMMAND.md +25 -0
  52. package/.pbai4devs/commands/implementar-entrega/COMMAND.md +20 -0
  53. package/.pbai4devs/commands/planejar-entrega/COMMAND.md +17 -0
  54. package/.pbai4devs/commands/preparar-seguranca/COMMAND.md +22 -0
  55. package/.pbai4devs/commands/validar-entrega/COMMAND.md +18 -0
  56. package/.pbai4devs/commands/verificar-dependencias/COMMAND.md +18 -0
  57. package/.pbai4devs/commands/verificar-segredos/COMMAND.md +19 -0
  58. package/.pbai4devs/external-skills/CATALOG.json +39 -1
  59. package/.pbai4devs/external-skills/README.md +13 -0
  60. package/.pbai4devs/rules/README.md +4 -0
  61. package/.pbai4devs/rules/execucao-local/RULE.md +20 -0
  62. package/.pbai4devs/rules/execucao-local/cursor.yaml +4 -0
  63. package/.pbai4devs/rules/seguranca/RULE.md +19 -0
  64. package/.pbai4devs/rules/seguranca/cursor.yaml +4 -0
  65. package/.pbai4devs/scripts/README.md +1 -0
  66. package/.pbai4devs/scripts/auditar-seguranca/README.md +57 -0
  67. package/.pbai4devs/scripts/auditar-seguranca/SCRIPT.mjs +411 -0
  68. package/.pbai4devs/scripts/auditar-seguranca/database-zip.mjs +143 -0
  69. package/.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml +50 -0
  70. package/.pbai4devs/scripts/auditar-seguranca/tests/SMOKE.mjs +74 -0
  71. package/.pbai4devs/scripts/auditar-seguranca/tests/TEST.mjs +247 -0
  72. package/.pbai4devs/scripts/auditar-seguranca/tests/zip-fixture.mjs +45 -0
  73. package/.pbai4devs/scripts/lib/native-release.mjs +193 -0
  74. package/.pbai4devs/scripts/preparar-ferramentas/README.md +8 -2
  75. package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +46 -16
  76. package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +2 -0
  77. package/.pbai4devs/scripts/preparar-ferramentas/tests/native-release.test.mjs +103 -0
  78. package/.pbai4devs/skills/README.md +10 -2
  79. package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +5 -1
  80. package/.pbai4devs/skills/seguranca/REFERENCE.md +104 -0
  81. package/.pbai4devs/skills/seguranca/corrigir-vulnerabilidade/SKILL.md +27 -0
  82. package/.pbai4devs/skills/seguranca/modelar-ameacas/SKILL.md +25 -0
  83. package/.pbai4devs/skills/seguranca/revisar-autorizacao/SKILL.md +29 -0
  84. package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +27 -0
  85. package/.pbai4devs/templates/README.md +2 -0
  86. package/.pbai4devs/templates/revisao-seguranca/TEMPLATE.md +33 -0
  87. package/.pbai4devs/tools/README.md +11 -3
  88. package/.pbai4devs/tools/gitleaks/README.md +22 -0
  89. package/.pbai4devs/tools/gitleaks/SCRIPT.mjs +5 -0
  90. package/.pbai4devs/tools/gitleaks/TOOL.json +67 -0
  91. package/.pbai4devs/tools/opengrep/README.md +22 -0
  92. package/.pbai4devs/tools/opengrep/SCRIPT.mjs +5 -0
  93. package/.pbai4devs/tools/opengrep/TOOL.json +71 -0
  94. package/.pbai4devs/tools/osv-scanner/README.md +23 -0
  95. package/.pbai4devs/tools/osv-scanner/SCRIPT.mjs +5 -0
  96. package/.pbai4devs/tools/osv-scanner/TOOL.json +70 -0
  97. package/CHANGELOG.md +17 -0
  98. package/README.en.md +18 -5
  99. package/README.es.md +19 -6
  100. package/README.md +16 -3
  101. package/docs/README.md +9 -5
  102. package/docs/guides/README.md +3 -1
  103. package/docs/guides/cli.md +3 -0
  104. package/docs/guides/installation.md +61 -11
  105. package/docs/guides/prerequisites.md +123 -0
  106. package/docs/guides/resources.md +48 -1
  107. package/docs/guides/security-references.md +97 -0
  108. package/docs/guides/security.md +32 -0
  109. package/docs/guides/tools-and-testing.md +2 -1
  110. package/docs/guides/troubleshooting.md +23 -7
  111. package/docs/maintainers/README.md +1 -1
  112. package/docs/maintainers/maintenance.md +15 -4
  113. package/docs/maintainers/packages.md +45 -13
  114. package/docs/product/prd.md +4 -1
  115. package/docs/product/roadmap.md +4 -0
  116. package/docs/specs/README.md +3 -0
  117. package/docs/specs/dual-registry/SPEC.md +43 -0
  118. package/docs/specs/security-ecosystem/README.md +9 -0
  119. package/docs/specs/security-ecosystem/SPEC.md +53 -0
  120. package/docs/specs/v0.0.10/README.md +9 -0
  121. package/docs/specs/v0.0.10/SPEC.md +42 -0
  122. package/package.json +1 -1
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+ import { constants, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readlinkSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
7
+ import { ToolManager } from '../preparar-ferramentas/SCRIPT.mjs';
8
+ import { checkLocalIgnore, planLocalIgnore } from '../lib/gitignore.mjs';
9
+ import { validateOsvDatabase } from './database-zip.mjs';
10
+
11
+ const checks = Object.freeze({ secrets: 'gitleaks', dependencies: 'osv-scanner', sast: 'opengrep' });
12
+ const ecosystemNames = ['npm', 'Go', 'PyPI', 'Maven', 'NuGet', 'Packagist', 'crates.io', 'RubyGems', 'Hex', 'Pub', 'Conan', 'Hackage'];
13
+ const ecosystems = new Map(ecosystemNames.map(name => [name.toLowerCase(), name]));
14
+ const discardedDirectories = new Set(['.git', '.agent-tests', 'node_modules', 'tmp', 'temp', '.cache', '.local', 'coverage', '__pycache__']);
15
+ const scannerConfiguration = new Set(['.semgrepignore', '.gitleaksignore', '.gitleaks.toml', 'osv-scanner.toml', '.osv-scanner.toml']);
16
+ const opengrepRules = new Set(['pbai-eval-input', 'pbai-shell-input', 'pbai-sql-input', 'pbai-tls-verification-disabled']);
17
+ const digest = value => createHash('sha256').update(value).digest('hex');
18
+ const portable = value => value.split(sep).join('/');
19
+ const fail = message => { const error = new Error(message); error.publicMessage = message; throw error; };
20
+ const stat = path => { try { return lstatSync(path); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
21
+ const parseJson = text => { try { return JSON.parse(text); } catch { fail('A ferramenta nao produziu JSON valido.'); } };
22
+ const identifier = value => typeof value === 'string' && /^[a-zA-Z0-9@._:/+ -]{1,180}$/.test(value) ? value : 'unknown';
23
+
24
+ export function parseArgs(input) {
25
+ const args = [...input], command = args.shift() || 'help';
26
+ if (['help', '--help', '-h'].includes(command)) {
27
+ if (args.length) fail('Ajuda nao recebe argumentos.');
28
+ return { command: 'help' };
29
+ }
30
+ if (!['run', 'check', 'update-data'].includes(command)) fail('Use run, check ou update-data.');
31
+ const result = { command, checks: Object.keys(checks), target: '.', includeHistory: false, ecosystems: [], output: null };
32
+ const seen = new Set();
33
+ while (args.length) {
34
+ const key = args.shift();
35
+ if (seen.has(key)) fail('Opcao repetida.');
36
+ seen.add(key);
37
+ if (key === '--include-history') result.includeHistory = true;
38
+ else if (['--checks', '--target', '--output', '--ecosystems'].includes(key)) {
39
+ const value = args.shift();
40
+ if (typeof value !== 'string' || !value || value.startsWith('--') || /[\x00-\x1f]/.test(value)) fail('Valor de opcao invalido.');
41
+ if (key === '--checks') {
42
+ result.checks = [...new Set(value.split(','))];
43
+ if (!result.checks.length || result.checks.some(name => !Object.hasOwn(checks, name))) fail('Checks aceitos: secrets,dependencies,sast.');
44
+ } else if (key === '--ecosystems') {
45
+ result.ecosystems = [...new Set(value.split(',').map(name => ecosystems.get(name.toLowerCase())))];
46
+ if (!result.ecosystems.length || result.ecosystems.includes(undefined)) fail('Ecossistema OSV nao autorizado.');
47
+ } else result[key === '--target' ? 'target' : 'output'] = value;
48
+ } else fail('Opcao desconhecida.');
49
+ }
50
+ if (command !== 'run' && (seen.has('--target') || result.includeHistory || result.output)) fail('Target, historico e output pertencem a run.');
51
+ if (command !== 'update-data' && result.ecosystems.length) fail('Ecossistemas pertencem a update-data.');
52
+ if (command === 'update-data' && (!result.ecosystems.length || seen.has('--checks'))) fail('Use update-data --ecosystems npm,Go.');
53
+ if (result.includeHistory && !result.checks.includes('secrets')) fail('Historico exige secrets.');
54
+ return result;
55
+ }
56
+
57
+ export function safePath(root, input, { directory = false, mustExist = false } = {}) {
58
+ if (typeof input !== 'string' || /[\x00-\x1f]/.test(input)) fail('Caminho invalido.');
59
+ const full = resolve(root, input), child = relative(root, full);
60
+ if (isAbsolute(child) || child === '..' || child.startsWith(`..${sep}`)) fail('Caminho fora da raiz autorizada.');
61
+ let current = root;
62
+ if (!stat(root)?.isDirectory() || stat(root).isSymbolicLink()) fail('Raiz deve ser um diretorio real.');
63
+ for (const part of child.split(sep).filter(Boolean)) {
64
+ current = join(current, part);
65
+ const item = stat(current);
66
+ if (item?.isSymbolicLink()) fail('Links simbolicos nao sao aceitos como caminho de execucao.');
67
+ if (item && current !== full && !item.isDirectory()) fail('Ancestral nao e um diretorio.');
68
+ }
69
+ const item = stat(full);
70
+ if (mustExist && !item) fail('Caminho inexistente.');
71
+ if (item && directory && !item.isDirectory()) fail('Caminho deve ser um diretorio.');
72
+ return full;
73
+ }
74
+
75
+ function mkdirSafe(root, input) {
76
+ const full = safePath(root, input, { directory: true });
77
+ let current = root;
78
+ for (const part of relative(root, full).split(sep).filter(Boolean)) {
79
+ current = join(current, part);
80
+ if (!stat(current)) mkdirSync(current, { mode: 0o700 });
81
+ if (!lstatSync(current).isDirectory() || lstatSync(current).isSymbolicLink()) fail('Diretorio local inseguro.');
82
+ }
83
+ return full;
84
+ }
85
+
86
+ export function sanitizedEnvironment(environment = process.env) {
87
+ const allowed = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'TMP', 'TEMP', 'TMPDIR', 'HOME', 'USERPROFILE', 'LOCALAPPDATA', 'LANG', 'LC_ALL', 'TZ', 'NUMBER_OF_PROCESSORS', 'PROCESSOR_ARCHITECTURE']);
88
+ const clean = Object.fromEntries(Object.entries(environment).filter(([key]) => allowed.has(key.toUpperCase())));
89
+ return { ...clean, SEMGREP_ENABLE_VERSION_CHECK: '0', OPENGREP_ENABLE_VERSION_CHECK: '0', SEMGREP_SEND_METRICS: 'off',
90
+ DISABLE_TELEMETRY: '1', DO_NOT_TRACK: '1', CI: '1', GIT_TERMINAL_PROMPT: '0' };
91
+ }
92
+
93
+ function assertIgnored(root, path) {
94
+ const where = portable(relative(root, path));
95
+ if (!where.startsWith('.agent-tests/')) fail('Relatorios devem ficar dentro de .agent-tests.');
96
+ const git = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root, encoding: 'utf8', windowsHide: true });
97
+ if (git.status === 0) {
98
+ const ignored = spawnSync('git', ['-c', 'core.excludesFile=', 'check-ignore', '--no-index', '-q', '--', where], { cwd: root, windowsHide: true });
99
+ const tracked = spawnSync('git', ['ls-files', '--error-unmatch', '--', where], { cwd: root, stdio: 'ignore', windowsHide: true });
100
+ if (ignored.status !== 0 || tracked.status === 0) fail('O destino de evidencias precisa estar ignorado e fora do indice Git.');
101
+ } else if (!checkLocalIgnore(planLocalIgnore(root)).ok) fail('Prepare a protecao local de artefatos antes de auditar.');
102
+ }
103
+
104
+ function readStable(root, path) {
105
+ safePath(root, path, { mustExist: true });
106
+ const first = lstatSync(path);
107
+ if (!first.isFile()) fail('Fonte deve ser um arquivo regular.');
108
+ const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
109
+ try {
110
+ const opened = fstatSync(descriptor);
111
+ if (opened.dev !== first.dev || opened.ino !== first.ino || !opened.isFile()) fail('Fonte mudou durante a leitura.');
112
+ const bytes = readFileSync(descriptor), after = fstatSync(descriptor);
113
+ if (after.size !== first.size || after.mtimeMs !== first.mtimeMs) fail('Fonte mudou durante a leitura.');
114
+ return bytes;
115
+ } finally { closeSync(descriptor); }
116
+ }
117
+
118
+ export function snapshotSources(root, target, destination, { maxFiles = 20000, maxFileBytes = 10 * 1024 * 1024, maxTotalBytes = 256 * 1024 * 1024 } = {}) {
119
+ const files = [], entries = [], excluded = []; let bytes = 0;
120
+ const walk = (folder, prefix = '') => {
121
+ for (const name of readdirSync(folder).sort()) {
122
+ const key = prefix ? `${prefix}/${name}` : name, source = join(folder, name), info = lstatSync(source);
123
+ entries.push({ path: key, type: info.isSymbolicLink() ? 'symlink' : info.isDirectory() ? 'directory' : info.isFile() ? 'file' : 'special', mode: info.mode,
124
+ ...(info.isSymbolicLink() ? { target: readlinkSync(source) } : {}) });
125
+ if (info.isSymbolicLink()) { excluded.push({ path: key, reason: 'symbolic-link' }); continue; }
126
+ if (info.isDirectory()) {
127
+ if (discardedDirectories.has(name.toLowerCase()) || /^\.playwright/i.test(name)) { excluded.push({ path: key, reason: 'generated-directory' }); continue; }
128
+ walk(source, key); continue;
129
+ }
130
+ if (!info.isFile()) { excluded.push({ path: key, reason: 'special-file' }); continue; }
131
+ if (scannerConfiguration.has(name.toLowerCase())) { excluded.push({ path: key, reason: 'scanner-configuration-overridden' }); continue; }
132
+ if (info.size > maxFileBytes || files.length >= maxFiles || bytes + info.size > maxTotalBytes) fail('Escopo excede os limites de leitura; reduza --target.');
133
+ const data = readStable(root, source);
134
+ if (data.subarray(0, 8192).includes(0)) { excluded.push({ path: key, reason: 'binary-file' }); continue; }
135
+ if (destination) {
136
+ const output = join(destination, key);
137
+ mkdirSafe(root, dirname(output)); writeFileSync(output, data, { flag: 'wx', mode: 0o600 });
138
+ }
139
+ files.push({ path: key, sha256: digest(data), source }); bytes += data.length;
140
+ }
141
+ };
142
+ walk(target);
143
+ return { files, entries, excluded, bytes };
144
+ }
145
+
146
+ function inventoryDigest(inventory) {
147
+ return digest(JSON.stringify({ entries: inventory.entries, files: inventory.files.map(({ path, sha256 }) => ({ path, sha256 })), excluded: inventory.excluded, bytes: inventory.bytes }));
148
+ }
149
+
150
+ function findingPath(value, base) {
151
+ if (typeof value !== 'string' || /[\x00-\x1f]/.test(value)) fail('Caminho de achado invalido.');
152
+ const path = isAbsolute(value) ? relative(base, value) : value;
153
+ const normalized = portable(path).replace(/^\.\//, '');
154
+ if (!normalized || normalized.split('/').some(part => part === '..' || part === '.') || normalized.includes('\\')) fail('Achado aponta para fora do escopo.');
155
+ return normalized;
156
+ }
157
+
158
+ function readReport(root, file) {
159
+ safePath(root, file, { mustExist: true });
160
+ const info = lstatSync(file);
161
+ if (!info.isFile() || info.size > 32 * 1024 * 1024) fail('Relatorio bruto invalido ou grande demais.');
162
+ return parseJson(readFileSync(file, 'utf8'));
163
+ }
164
+
165
+ function finding(tool, rule, path, line, severity, message, extra = {}) {
166
+ rule = identifier(rule);
167
+ return { tool, rule, path, line: Number.isInteger(line) && line > 0 ? line : null,
168
+ severity: ['high', 'medium', 'low', 'info'].includes(severity) ? severity : 'medium', message,
169
+ fingerprint: digest(`${tool}:${rule}:${path}:${line || ''}`), ...extra };
170
+ }
171
+
172
+ export function parseGitleaks(value, status, base, files = 0) {
173
+ if (!Array.isArray(value) || ![0, 1].includes(status)) fail('Gitleaks nao concluiu uma analise valida.');
174
+ if (status === 1 && !value.length) fail('Gitleaks falhou sem achados verificaveis.');
175
+ const findings = value.map(item => {
176
+ if (!item || typeof item.RuleID !== 'string' || typeof item.File !== 'string') fail('Resultado Gitleaks incompleto.');
177
+ return finding('gitleaks', item.RuleID, findingPath(item.File, base), item.StartLine, 'high', 'Possivel segredo; confirmar contexto e necessidade de revogacao.');
178
+ });
179
+ return { status: findings.length ? 'findings' : files ? 'passed' : 'not-applicable', findings, scannedFiles: files };
180
+ }
181
+
182
+ export function parseOsv(value, status, base) {
183
+ if (![0, 1, 128].includes(status) || !value || !Array.isArray(value.results)) fail('OSV nao concluiu uma analise valida.');
184
+ const findings = []; let packages = 0;
185
+ for (const result of value.results) {
186
+ if (!result?.source || !Array.isArray(result.packages)) fail('Resultado OSV incompleto.');
187
+ const path = findingPath(result.source.path, base);
188
+ for (const item of result.packages) {
189
+ packages++;
190
+ if (!item.package || !Array.isArray(item.vulnerabilities || [])) fail('Pacote OSV incompleto.');
191
+ for (const vulnerability of item.vulnerabilities || []) {
192
+ if (typeof vulnerability.id !== 'string') fail('Vulnerabilidade OSV sem identificador.');
193
+ findings.push(finding('osv-scanner', vulnerability.id, path, null, 'medium', 'Dependencia com vulnerabilidade conhecida.',
194
+ { package: identifier(item.package.name), version: identifier(item.package.version), ecosystem: identifier(item.package.ecosystem) }));
195
+ }
196
+ }
197
+ }
198
+ if (status === 128 && packages || status === 1 && !findings.length) fail('Saida OSV inconsistente.');
199
+ return { status: findings.length ? 'findings' : packages ? 'passed' : 'not-applicable', findings, scannedPackages: packages };
200
+ }
201
+
202
+ export function parseOpengrep(value, status, base) {
203
+ if (!value || !Array.isArray(value.results) || !Array.isArray(value.errors) || ![0, 1].includes(status)) fail('Opengrep nao concluiu uma analise valida.');
204
+ if (value.errors.length) fail('Opengrep relatou erros de analise.');
205
+ if (!Array.isArray(value.paths?.scanned)) fail('Opengrep nao confirmou os arquivos analisados.');
206
+ const findings = value.results.map(item => {
207
+ if (typeof item?.check_id !== 'string' || typeof item.path !== 'string') fail('Resultado Opengrep incompleto.');
208
+ // Opengrep prefixes local rule IDs with the dotted absolute config path.
209
+ const rule = item.check_id.slice(item.check_id.lastIndexOf('.') + 1);
210
+ if (!opengrepRules.has(rule)) fail('Identificador de regra Opengrep desconhecido.');
211
+ return finding('opengrep', rule, findingPath(item.path, base), item.start?.line,
212
+ item.extra?.severity === 'ERROR' ? 'high' : 'medium', 'Padrao de codigo exige revisao de seguranca.');
213
+ });
214
+ if (status === 1 && !findings.length) fail('Opengrep falhou sem achados verificaveis.');
215
+ return { status: findings.length ? 'findings' : value.paths.scanned.length ? 'passed' : 'not-applicable', findings, scannedFiles: value.paths.scanned.length };
216
+ }
217
+
218
+ export function databaseCache(manager) {
219
+ const entry = manager.entry('osv-scanner'), where = entry.envPaths?.OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY;
220
+ if (typeof where !== 'string' || !where) fail('Cache OSV nao configurado.');
221
+ return safePath(manager.root, join(manager.local('osv-scanner'), where), { directory: true });
222
+ }
223
+
224
+ export function verifyDatabase(root, cache, ecosystem) {
225
+ if (!ecosystemNames.includes(ecosystem)) fail('Ecossistema OSV nao autorizado.');
226
+ const folder = safePath(root, join(cache, 'osv-scalibr', ecosystem), { directory: true });
227
+ const archive = safePath(root, join(folder, 'all.zip'), { mustExist: true });
228
+ const receiptPath = safePath(root, join(folder, 'DATABASE.json'), { mustExist: true });
229
+ const receipt = parseJson(readFileSync(receiptPath, 'utf8'));
230
+ const bytes = readFileSync(archive);
231
+ if (receipt.ecosystem !== ecosystem || receipt.source !== `https://osv-vulnerabilities.storage.googleapis.com/${encodeURIComponent(ecosystem)}/all.zip`
232
+ || receipt.sha256 !== digest(bytes) || receipt.bytes !== bytes.length) fail('Base OSV ausente, alterada ou sem recibo; execute update-data.');
233
+ const validation = receipt.validation?.format === 'osv-zip-v1' ? receipt.validation : validateOsvDatabase(bytes);
234
+ return { ecosystem, sha256: receipt.sha256, bytes: receipt.bytes, downloadedAt: receipt.downloadedAt, validation };
235
+ }
236
+
237
+ export function detectEcosystems(files) {
238
+ const detected = new Set();
239
+ for (const file of files) {
240
+ const name = file.path.split('/').at(-1).toLowerCase();
241
+ if (['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb'].includes(name)) detected.add('npm');
242
+ if (name === 'go.sum' || name === 'go.mod') detected.add('Go');
243
+ if (['requirements.txt', 'poetry.lock', 'uv.lock', 'pipfile.lock', 'pdm.lock'].includes(name)) detected.add('PyPI');
244
+ if (['pom.xml', 'gradle.lockfile'].includes(name)) detected.add('Maven');
245
+ if (['packages.lock.json', 'packages.config'].includes(name)) detected.add('NuGet');
246
+ if (name === 'composer.lock') detected.add('Packagist');
247
+ if (name === 'cargo.lock') detected.add('crates.io');
248
+ if (name === 'gemfile.lock') detected.add('RubyGems');
249
+ if (name === 'mix.lock') detected.add('Hex');
250
+ if (name === 'pubspec.lock') detected.add('Pub');
251
+ if (name === 'conan.lock') detected.add('Conan');
252
+ if (/^.*\.cabal$/.test(name)) detected.add('Hackage');
253
+ }
254
+ return [...detected].sort();
255
+ }
256
+
257
+ async function downloadDatabase(root, cache, ecosystem, fetcher) {
258
+ const source = `https://osv-vulnerabilities.storage.googleapis.com/${encodeURIComponent(ecosystem)}/all.zip`;
259
+ const response = await fetcher(source, { redirect: 'error', credentials: 'omit', headers: { Accept: 'application/zip' }, signal: AbortSignal.timeout(120000) });
260
+ if (response.status !== 200 || !response.body) fail('Download da base OSV falhou.');
261
+ const limit = 512 * 1024 * 1024, declared = Number(response.headers.get('content-length'));
262
+ if (Number.isFinite(declared) && declared > limit) fail('Base OSV excede o limite de download.');
263
+ const reader = response.body.getReader(), chunks = []; let size = 0;
264
+ try {
265
+ while (true) { const next = await reader.read(); if (next.done) break; size += next.value.length; if (size > limit) fail('Base OSV excede o limite de download.'); chunks.push(Buffer.from(next.value)); }
266
+ } finally { await reader.cancel().catch(() => {}); }
267
+ const bytes = Buffer.concat(chunks);
268
+ const validation = validateOsvDatabase(bytes);
269
+ const folder = mkdirSafe(root, join(cache, 'osv-scalibr', ecosystem));
270
+ const target = safePath(root, join(folder, 'all.zip')), receipt = safePath(root, join(folder, 'DATABASE.json'));
271
+ for (const path of [target, receipt]) if (stat(path) && !stat(path).isFile()) fail('Estado da base OSV invalido.');
272
+ const token = randomUUID(), staged = join(folder, `.all-${token}.zip`), stagedReceipt = join(folder, `.DATABASE-${token}.json`);
273
+ const previous = `${target}.previous-${token}`, previousReceipt = `${receipt}.previous-${token}`;
274
+ const data = { schemaVersion: 1, ecosystem, source, sha256: digest(bytes), bytes: bytes.length, downloadedAt: new Date().toISOString(), validation,
275
+ integrity: 'HTTPS oficial e SHA256 local; nao e assinatura do publicador.' };
276
+ let moved = false, movedReceipt = false, promoted = false;
277
+ try {
278
+ writeFileSync(staged, bytes, { flag: 'wx', mode: 0o600 }); writeFileSync(stagedReceipt, `${JSON.stringify(data, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
279
+ if (existsSync(target)) { renameSync(target, previous); moved = true; }
280
+ if (existsSync(receipt)) { renameSync(receipt, previousReceipt); movedReceipt = true; }
281
+ renameSync(staged, target); promoted = true; renameSync(stagedReceipt, receipt);
282
+ try { rmSync(previous, { force: true }); } catch { /* Versao antiga ignorada pode ser limpa depois. */ }
283
+ try { rmSync(previousReceipt, { force: true }); } catch { /* Nao desfazer a promocao por falha de limpeza. */ }
284
+ return data;
285
+ } catch (error) {
286
+ if (promoted) rmSync(target, { force: true });
287
+ if (moved) renameSync(previous, target);
288
+ if (movedReceipt) { rmSync(receipt, { force: true }); renameSync(previousReceipt, receipt); }
289
+ throw error;
290
+ } finally { rmSync(staged, { force: true }); rmSync(stagedReceipt, { force: true }); }
291
+ }
292
+
293
+ export class SecurityAudit {
294
+ constructor({ root = projectRoot, manager, execute = spawnSync, fetcher = fetch, snapshot = snapshotSources } = {}) {
295
+ requireNode(); this.root = realpathSync(root); this.manager = manager || new ToolManager({ root: this.root });
296
+ this.execute = execute; this.fetcher = fetcher; this.snapshot = snapshot;
297
+ }
298
+ check(selected = Object.keys(checks)) {
299
+ if (!Array.isArray(selected) || !selected.length || selected.some(name => !Object.hasOwn(checks, name))) fail('Selecao de checks invalida.');
300
+ const results = selected.map(name => {
301
+ try { const info = this.manager.check(checks[name]); return { check: name, tool: checks[name], status: 'passed', version: info.version }; }
302
+ catch { return { check: name, tool: checks[name], status: 'not-run', reason: 'Ferramenta ausente ou invalida; execute preparar-ferramentas setup security.' }; }
303
+ });
304
+ return { status: results.some(item => item.status !== 'passed') ? 'incomplete' : 'passed', scope: 'installed-tools', analysisPerformed: false, offline: true, results, exitCode: results.some(item => item.status !== 'passed') ? 2 : 0 };
305
+ }
306
+ invoke(name, args, cwd) {
307
+ const script = safePath(this.root, `.pbai4devs/tools/${name}/SCRIPT.mjs`, { mustExist: true });
308
+ const result = this.execute(process.execPath, [script, 'run', ...args], { cwd, env: sanitizedEnvironment(), encoding: 'utf8',
309
+ timeout: 180000, maxBuffer: 32 * 1024 * 1024, shell: false, windowsHide: true });
310
+ if (!result || result.error || result.signal || !Number.isInteger(result.status)) fail('Ferramenta interrompida ou indisponivel.');
311
+ return result;
312
+ }
313
+ async updateData(selected) {
314
+ this.manager.check('osv-scanner'); const cache = databaseCache(this.manager);
315
+ if (!Array.isArray(selected) || !selected.length || selected.some(name => !ecosystemNames.includes(name))) fail('Selecione ecossistemas OSV validos.');
316
+ return this.manager.withLock('osv-scanner', async () => {
317
+ mkdirSafe(this.root, cache); const results = [];
318
+ for (const ecosystem of selected) results.push(await downloadDatabase(this.root, cache, ecosystem, this.fetcher));
319
+ return { status: 'passed', mode: 'update-data', analysisPerformed: false, results, exitCode: 0 };
320
+ });
321
+ }
322
+ async run(options = {}) {
323
+ const selected = options.checks || Object.keys(checks), target = safePath(this.root, options.target || '.', { directory: true, mustExist: true });
324
+ if (!Array.isArray(selected) || !selected.length || selected.some(name => !Object.hasOwn(checks, name))) fail('Selecao de checks invalida.');
325
+ const workBasePath = resolve(this.root, '.agent-tests/security');
326
+ const contains = (parent, child) => { const part = relative(parent, child); return !isAbsolute(part) && part !== '..' && !part.startsWith(`..${sep}`); };
327
+ if (target !== this.root && (contains(target, workBasePath) || contains(workBasePath, target))) fail('Target conflita com a pasta reservada de evidencias.');
328
+ if (options.includeHistory) {
329
+ const git = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: target, encoding: 'utf8', windowsHide: true });
330
+ if (git.status !== 0 || realpathSync(git.stdout.trim()) !== target) fail('Historico exige --target na raiz do repositorio Git.');
331
+ }
332
+ const execution = `${new Date().toISOString().replace(/[^a-z0-9]/gi, '').toLowerCase()}-${randomUUID().slice(0, 8)}`;
333
+ const output = safePath(this.root, options.output || `.agent-tests/security/${execution}/report.json`);
334
+ if (!output.endsWith('.json')) fail('Relatorio exige extensao .json.');
335
+ assertIgnored(this.root, output); if (stat(output)) fail('Relatorio ja existe; escolha outra execucao.');
336
+ const workBase = mkdirSafe(this.root, workBasePath);
337
+ const work = mkdtempSync(join(workBase, '.work-')); const source = mkdirSafe(this.root, join(work, 'source'));
338
+ const report = { schemaVersion: 1, mode: 'run', status: 'incomplete', target: portable(relative(this.root, target)) || '.',
339
+ offline: true, execution, generatedAt: new Date().toISOString(), checks: [], findings: [], coverage: null,
340
+ limits: ['Analise do snapshot de arquivos de texto; binarios, links e diretorios gerados nao sao varridos.',
341
+ 'Regras SAST proprias cobrem padroes JS/TS selecionados. Scanner nao comprova autorizacao, RLS, regras Firebase ou seguranca total.'] };
342
+ try {
343
+ const inventory = this.snapshot(this.root, target, source);
344
+ report.coverage = { files: inventory.files.length, bytes: inventory.bytes, excluded: inventory.excluded,
345
+ sourceDigest: digest(inventory.files.map(file => `${file.path}:${file.sha256}`).join('\n')), inventoryDigest: inventoryDigest(inventory) };
346
+ for (const name of selected) {
347
+ const tool = checks[name]; let version;
348
+ try {
349
+ version = this.manager.check(tool).version;
350
+ let parsed;
351
+ if (name === 'secrets') {
352
+ const file = join(work, 'gitleaks.json');
353
+ const config = join(work, 'gitleaks.toml'); writeFileSync(config, '[extend]\nuseDefault = true\n', { mode: 0o600 });
354
+ const result = this.invoke(tool, ['dir', source, '--config', config, '--report-format=json', `--report-path=${file}`, '--redact=100', '--no-banner', '--no-color', '--log-level=error', '--ignore-gitleaks-allow', '--timeout=150'], work);
355
+ if (!stat(file)?.isFile()) fail('Gitleaks nao confirmou o relatorio.');
356
+ parsed = parseGitleaks(readReport(this.root, file), result.status, source, inventory.files.length);
357
+ if (options.includeHistory) {
358
+ const file = join(work, 'history.json');
359
+ const result = this.invoke(tool, ['git', target, '--config', config, '--report-format=json', `--report-path=${file}`, '--redact=100', '--no-banner', '--no-color', '--log-level=error', '--ignore-gitleaks-allow', '--timeout=150', '--log-opts=--all --no-ext-diff --no-textconv'], work);
360
+ if (!stat(file)?.isFile()) fail('Gitleaks nao confirmou o historico.');
361
+ const history = parseGitleaks(readReport(this.root, file), result.status, target, 1);
362
+ parsed.findings.push(...history.findings.map(item => ({ ...item, scope: 'git-history' })));
363
+ parsed.status = parsed.findings.length ? 'findings' : parsed.status;
364
+ }
365
+ } else if (name === 'dependencies') {
366
+ const needed = detectEcosystems(inventory.files);
367
+ if (!needed.length) { parsed = { status: 'not-applicable', findings: [], reason: 'Nenhum lockfile ou manifesto reconhecido neste escopo.' }; }
368
+ else {
369
+ const cache = databaseCache(this.manager); let databases;
370
+ try { databases = needed.map(ecosystem => verifyDatabase(this.root, cache, ecosystem)); }
371
+ catch { fail('Base OSV ausente ou alterada; execute update-data para os ecossistemas do projeto.'); }
372
+ const config = join(work, 'osv.toml'), file = join(work, 'osv.json'); writeFileSync(config, '', { mode: 0o600 });
373
+ const result = this.invoke(tool, ['scan', 'source', '--offline', '--no-resolve', '--no-ignore', '--x-no-default-plugins', '--x-plugins=lockfile,sbom', '--all-packages', '--format=json', `--output-file=${file}`, `--config=${config}`, '-r', source], work);
374
+ if (result.status === 128 && !stat(file)) parsed = { status: 'not-applicable', findings: [], scannedPackages: 0,
375
+ reason: 'OSV nao encontrou pacotes analisaveis no escopo.', databases };
376
+ else {
377
+ if (!stat(file)?.isFile()) fail('OSV nao confirmou o relatorio.');
378
+ parsed = { ...parseOsv(readReport(this.root, file), result.status, source), databases };
379
+ }
380
+ for (const database of databases) if (verifyDatabase(this.root, cache, database.ecosystem).sha256 !== database.sha256) fail('Base OSV mudou durante a analise.');
381
+ }
382
+ } else {
383
+ const rule = safePath(this.root, '.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml', { mustExist: true });
384
+ const result = this.invoke(tool, ['--disable-version-check', '--no-git-ignore', '--disable-nosem', '--config', rule, '--json', '--error', '--quiet', '--jobs=2', source], work);
385
+ parsed = parseOpengrep(parseJson(result.stdout), result.status, source);
386
+ }
387
+ if (parsed.status === 'not-applicable' && !parsed.reason) parsed.reason = 'Nenhum arquivo ou pacote aplicavel foi confirmado neste escopo.';
388
+ report.checks.push({ check: name, tool, version, ...parsed, findings: parsed.findings.length }); report.findings.push(...parsed.findings);
389
+ } catch (error) { report.checks.push({ check: name, tool, ...(version ? { version } : {}), status: 'not-run', reason: error.publicMessage || 'Analise incompleta. Confira ferramenta, base local e formatos suportados; nenhuma aprovacao foi emitida.' }); }
390
+ }
391
+ const finalInventory = this.snapshot(this.root, target, null);
392
+ if (inventoryDigest(finalInventory) !== inventoryDigest(inventory)) fail('Inventario ou fontes mudaram durante a analise.');
393
+ report.status = report.checks.some(check => check.status === 'not-run') ? 'incomplete' : report.findings.length ? 'findings'
394
+ : report.checks.some(check => check.status === 'passed') ? 'passed' : 'not-applicable';
395
+ } catch (error) { report.status = 'incomplete'; report.error = error.publicMessage || 'Nao foi possivel concluir o snapshot ou confirmar fontes estaveis.'; }
396
+ finally { rmSync(work, { recursive: true, force: true }); }
397
+ report.exitCode = report.status === 'incomplete' ? 2 : report.status === 'findings' ? 1 : 0;
398
+ mkdirSafe(this.root, dirname(output)); writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
399
+ return { ...report, report: portable(relative(this.root, output)) };
400
+ }
401
+ }
402
+
403
+ const help = 'Uso: run [--checks secrets,dependencies,sast] [--target .] [--include-history] [--output .agent-tests/.../report.json]; check [--checks ...]; update-data --ecosystems npm,Go';
404
+ export async function main(args = process.argv.slice(2), options = {}) {
405
+ const parsed = parseArgs(args); if (parsed.command === 'help') { console.log(help); return 0; }
406
+ const audit = options.audit || new SecurityAudit(options);
407
+ const result = parsed.command === 'run' ? await audit.run(parsed) : parsed.command === 'check' ? audit.check(parsed.checks) : await audit.updateData(parsed.ecosystems);
408
+ const summary = parsed.command === 'run' ? { status: result.status, report: result.report, offline: true, findings: result.findings.length, checks: result.checks, exitCode: result.exitCode } : result;
409
+ console.log(JSON.stringify(summary)); return result.exitCode;
410
+ }
411
+ if (isMain(import.meta.url)) main().then(code => { process.exitCode = code; }).catch(error => { console.error(JSON.stringify({ status: 'incomplete', message: error.publicMessage || 'Falha no comando de seguranca; confira argumentos, caminhos e pre-requisitos.', exitCode: 2 })); process.exitCode = 2; });
@@ -0,0 +1,143 @@
1
+ import { crc32, inflateRawSync } from 'node:zlib';
2
+
3
+ const fail = message => { throw new Error(message); };
4
+ const decoder = new TextDecoder('utf-8', { fatal: true });
5
+ const MAX_ENTRY = 16 * 1024 * 1024;
6
+ const MAX_TOTAL = 2 * 1024 * 1024 * 1024;
7
+ const MAX_ENTRIES = 1000000;
8
+
9
+ function within(data, offset, length) {
10
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > data.length) fail('Estrutura ZIP fora dos limites.');
11
+ }
12
+ function number64(data, offset) {
13
+ within(data, offset, 8); const value = data.readBigUInt64LE(offset);
14
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) fail('Inteiro ZIP64 excede o limite seguro.');
15
+ return Number(value);
16
+ }
17
+ function extras(data, offset, size) {
18
+ within(data, offset, size); const end = offset + size; let zip64;
19
+ while (offset < end) {
20
+ if (offset + 4 > end) fail('Campo extra ZIP truncado.');
21
+ const id = data.readUInt16LE(offset), length = data.readUInt16LE(offset + 2); offset += 4;
22
+ if (offset + length > end) fail('Campo extra ZIP fora dos limites.');
23
+ if (id === 1) { if (zip64) fail('Campo ZIP64 duplicado.'); zip64 = data.subarray(offset, offset + length); }
24
+ offset += length;
25
+ }
26
+ return zip64;
27
+ }
28
+ function sizes(extra, rawSize, rawCompressed, rawOffset, rawDisk = 0) {
29
+ let position = 0;
30
+ const read = (raw, sentinel, width) => {
31
+ if (raw !== sentinel) return raw;
32
+ if (!extra || position + width > extra.length) fail('Campo ZIP64 obrigatorio ausente.');
33
+ const result = width === 8 ? number64(extra, position) : extra.readUInt32LE(position); position += width;
34
+ return result;
35
+ };
36
+ return { size: read(rawSize, 0xffffffff, 8), compressed: read(rawCompressed, 0xffffffff, 8),
37
+ offset: rawOffset === undefined ? undefined : read(rawOffset, 0xffffffff, 8), disk: read(rawDisk, 0xffff, 4) };
38
+ }
39
+ function directory(data) {
40
+ let end = -1;
41
+ for (let offset = data.length - 22; offset >= Math.max(0, data.length - 65557); offset--) {
42
+ if (data.readUInt32LE(offset) === 0x06054b50 && offset + 22 + data.readUInt16LE(offset + 20) === data.length) { end = offset; break; }
43
+ }
44
+ if (end < 0 || data.readUInt16LE(end + 4) || data.readUInt16LE(end + 6)) fail('Fim ZIP invalido ou multipartes.');
45
+ let count = data.readUInt16LE(end + 10), diskCount = data.readUInt16LE(end + 8), size = data.readUInt32LE(end + 12), offset = data.readUInt32LE(end + 16), stop = end;
46
+ const hasLocator = end >= 20 && data.readUInt32LE(end - 20) === 0x07064b50;
47
+ if ([count, diskCount].includes(0xffff) || [size, offset].includes(0xffffffff) || hasLocator) {
48
+ if (!hasLocator || data.readUInt32LE(end - 16) !== 0 || data.readUInt32LE(end - 4) !== 1) fail('Localizador ZIP64 invalido.');
49
+ stop = number64(data, end - 12); within(data, stop, 56);
50
+ if (data.readUInt32LE(stop) !== 0x06064b50) fail('Fim ZIP64 ausente.');
51
+ const recordLength = number64(data, stop + 4);
52
+ if (recordLength < 44 || stop + 12 + recordLength !== end - 20 || data.readUInt32LE(stop + 16) || data.readUInt32LE(stop + 20)) fail('Fim ZIP64 invalido ou multipartes.');
53
+ const actualDiskCount = number64(data, stop + 24), actualCount = number64(data, stop + 32), actualSize = number64(data, stop + 40), actualOffset = number64(data, stop + 48);
54
+ if (actualDiskCount !== actualCount || count !== 0xffff && count !== actualCount || diskCount !== 0xffff && diskCount !== actualDiskCount
55
+ || size !== 0xffffffff && size !== actualSize || offset !== 0xffffffff && offset !== actualOffset) fail('Metadados ZIP e ZIP64 divergem.');
56
+ count = actualCount; diskCount = actualDiskCount; size = actualSize; offset = actualOffset;
57
+ }
58
+ if (!count || count > MAX_ENTRIES || diskCount !== count || offset + size !== stop) fail('Diretorio ZIP invalido.');
59
+ within(data, offset, size); return { count, offset, stop };
60
+ }
61
+ function entryName(bytes, isDirectory) {
62
+ let name; try { name = decoder.decode(bytes); } catch { fail('Nome ZIP nao e UTF-8 valido.'); }
63
+ const path = isDirectory ? name.slice(0, -1) : name;
64
+ if (!path || name.length > 1024 || /[\\\x00-\x1f:]/.test(name) || path.split('/').some(part => !part || part === '.' || part === '..')) fail('Nome de entrada ZIP inseguro.');
65
+ return name;
66
+ }
67
+ function record(data, name) {
68
+ let value; try { value = JSON.parse(decoder.decode(data)); } catch { fail('Registro da base nao e JSON UTF-8 valido.'); }
69
+ const timestamp = value => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
70
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.id !== 'string' || !value.id || value.id.length > 512
71
+ || !timestamp(value.modified) || name.split('/').at(-1) !== `${value.id}.json`) fail('Registro JSON nao corresponde ao formato OSV.');
72
+ if (value.affected !== undefined && (!Array.isArray(value.affected) || value.affected.some(item => !item || typeof item !== 'object' || Array.isArray(item)))) fail('Affected OSV invalido.');
73
+ if (value.withdrawn !== undefined && !timestamp(value.withdrawn)) fail('Withdrawn OSV invalido.');
74
+ }
75
+ function descriptorEnd(data, start, crc, size, compressed, central) {
76
+ const candidates = [];
77
+ for (const signature of [false, true]) for (const wide of [false, true]) {
78
+ const length = (signature ? 4 : 0) + 4 + (wide ? 16 : 8);
79
+ if (start + length > central) continue;
80
+ let offset = start;
81
+ if (signature) { if (data.readUInt32LE(offset) !== 0x08074b50) continue; offset += 4; }
82
+ if (data.readUInt32LE(offset) !== crc) continue; offset += 4;
83
+ const c = wide ? data.readBigUInt64LE(offset) : data.readUInt32LE(offset); offset += wide ? 8 : 4;
84
+ const s = wide ? data.readBigUInt64LE(offset) : data.readUInt32LE(offset);
85
+ if (c === (wide ? BigInt(compressed) : compressed) && s === (wide ? BigInt(size) : size)) candidates.push(start + length);
86
+ }
87
+ if (candidates.length !== 1) fail('Data descriptor ZIP invalido ou ambiguo.');
88
+ return candidates[0];
89
+ }
90
+
91
+ // Parse without extracting. Every JSON record, payload CRC and range is checked.
92
+ // ZIP64 is required by the official npm dataset (> 65,535 records).
93
+ export function validateOsvDatabase(data, { maxEntryBytes = MAX_ENTRY, maxTotalBytes = MAX_TOTAL, maxEntries = MAX_ENTRIES } = {}) {
94
+ if (!Buffer.isBuffer(data) || data.length < 22 || data.length > 512 * 1024 * 1024) fail('Tamanho ZIP OSV invalido.');
95
+ for (const value of [maxEntryBytes, maxTotalBytes, maxEntries]) if (!Number.isSafeInteger(value) || value < 1) fail('Limite ZIP invalido.');
96
+ const central = directory(data); if (central.count > maxEntries) fail('Numero de entradas ZIP excede o limite.');
97
+ const names = new Set(), intervals = []; let offset = central.offset, records = 0, total = 0;
98
+ for (let index = 0; index < central.count; index++) {
99
+ within(data, offset, 46);
100
+ if (offset + 46 > central.stop || data.readUInt32LE(offset) !== 0x02014b50) fail('Entrada central ZIP invalida.');
101
+ const flags = data.readUInt16LE(offset + 8), method = data.readUInt16LE(offset + 10), crc = data.readUInt32LE(offset + 16);
102
+ const rawCompressed = data.readUInt32LE(offset + 20), rawSize = data.readUInt32LE(offset + 24);
103
+ const nameLength = data.readUInt16LE(offset + 28), extraLength = data.readUInt16LE(offset + 30), commentLength = data.readUInt16LE(offset + 32);
104
+ const rawDisk = data.readUInt16LE(offset + 34), attributes = data.readUInt32LE(offset + 38), rawLocal = data.readUInt32LE(offset + 42);
105
+ const next = offset + 46 + nameLength + extraLength + commentLength;
106
+ if (next > central.stop || flags & ~0x080e || ![0, 8].includes(method)) fail('Flags ou metodo ZIP nao suportados.');
107
+ const nameBytes = data.subarray(offset + 46, offset + 46 + nameLength), isDirectory = nameBytes.at(-1) === 47;
108
+ const name = entryName(nameBytes, isDirectory), type = (attributes >>> 16) & 0o170000;
109
+ if (![0, 0o100000, 0o040000].includes(type) || (type === 0o040000 || attributes & 0x10) && !isDirectory) fail('Link ou arquivo especial em ZIP OSV.');
110
+ if (names.has(name.toLowerCase())) fail('Nome ZIP duplicado.'); names.add(name.toLowerCase());
111
+ const extra = extras(data, offset + 46 + nameLength, extraLength);
112
+ const item = sizes(extra, rawSize, rawCompressed, rawLocal, rawDisk);
113
+ if (item.disk || item.size > maxEntryBytes || item.compressed > data.length || (total += item.size) > maxTotalBytes) fail('Entradas ZIP excedem limites ou discos.');
114
+ within(data, item.offset, 30);
115
+ if (item.offset + 30 > central.offset || data.readUInt32LE(item.offset) !== 0x04034b50 || data.readUInt16LE(item.offset + 6) !== flags || data.readUInt16LE(item.offset + 8) !== method) fail('Cabecalho local ZIP diverge do central.');
116
+ const localNameLength = data.readUInt16LE(item.offset + 26), localExtraLength = data.readUInt16LE(item.offset + 28);
117
+ const start = item.offset + 30 + localNameLength + localExtraLength, payloadEnd = start + item.compressed;
118
+ if (payloadEnd > central.offset || !data.subarray(item.offset + 30, item.offset + 30 + localNameLength).equals(nameBytes)) fail('Payload ou nome local ZIP invalido.');
119
+ const localExtra = extras(data, item.offset + 30 + localNameLength, localExtraLength);
120
+ const localCrc = data.readUInt32LE(item.offset + 14), localCompressed = data.readUInt32LE(item.offset + 18), localSize = data.readUInt32LE(item.offset + 22);
121
+ let end = payloadEnd;
122
+ if (!(flags & 8)) {
123
+ const local = sizes(localExtra, localSize, localCompressed);
124
+ if (localCrc !== crc || local.size !== item.size || local.compressed !== item.compressed) fail('Tamanhos ou CRC locais ZIP divergem.');
125
+ } else {
126
+ if (localCrc !== 0 && localCrc !== crc) fail('CRC local ZIP invalido.');
127
+ end = descriptorEnd(data, payloadEnd, crc, item.size, item.compressed, central.offset);
128
+ }
129
+ intervals.push({ start: item.offset, end });
130
+ let bytes;
131
+ try { bytes = method === 0 ? data.subarray(start, payloadEnd) : inflateRawSync(data.subarray(start, payloadEnd), { maxOutputLength: maxEntryBytes }); }
132
+ catch { fail('Payload ZIP nao pode ser descompactado com os limites definidos.'); }
133
+ if (bytes.length !== item.size || crc32(bytes) !== crc) fail('Integridade CRC/tamanho do payload ZIP invalida.');
134
+ if (isDirectory) { if (bytes.length) fail('Diretorio ZIP com conteudo.'); }
135
+ else { if (!name.endsWith('.json')) fail('Base OSV contem arquivo que nao e JSON.'); record(bytes, name); records++; }
136
+ offset = next;
137
+ }
138
+ if (offset !== central.stop || !records) fail('Diretorio ZIP incompleto ou sem registros OSV.');
139
+ intervals.sort((a, b) => a.start - b.start); let previous = 0;
140
+ for (const interval of intervals) { if (interval.start !== previous || interval.end <= interval.start) fail('Payloads ZIP sobrepostos, ocultos ou com lacunas.'); previous = interval.end; }
141
+ if (previous !== central.offset) fail('Payload ZIP nao cobre o diretorio declarado.');
142
+ return { format: 'osv-zip-v1', entries: central.count, records, compressedBytes: data.length, uncompressedBytes: total };
143
+ }
@@ -0,0 +1,50 @@
1
+ rules:
2
+ - id: pbai-eval-input
3
+ languages: [javascript, typescript]
4
+ severity: ERROR
5
+ message: Entrada HTTP chega a execucao dinamica de codigo.
6
+ mode: taint
7
+ pattern-sources:
8
+ - pattern-either:
9
+ - pattern: $REQ.query.$FIELD
10
+ - pattern: $REQ.body.$FIELD
11
+ - pattern: $REQ.params.$FIELD
12
+ pattern-sinks:
13
+ - pattern-either:
14
+ - pattern: eval($INPUT)
15
+ - pattern: new Function(..., $INPUT)
16
+ - id: pbai-shell-input
17
+ languages: [javascript, typescript]
18
+ severity: ERROR
19
+ message: Entrada HTTP chega a um comando interpretado pelo shell.
20
+ mode: taint
21
+ pattern-sources:
22
+ - pattern-either:
23
+ - pattern: $REQ.query.$FIELD
24
+ - pattern: $REQ.body.$FIELD
25
+ - pattern: $REQ.params.$FIELD
26
+ pattern-sinks:
27
+ - pattern-either:
28
+ - pattern: exec($INPUT, ...)
29
+ - pattern: $CP.exec($INPUT, ...)
30
+ - pattern: execSync($INPUT, ...)
31
+ - pattern: $CP.execSync($INPUT, ...)
32
+ - id: pbai-sql-input
33
+ languages: [javascript, typescript]
34
+ severity: ERROR
35
+ message: Entrada HTTP e concatenada numa consulta SQL.
36
+ mode: taint
37
+ pattern-sources:
38
+ - pattern-either:
39
+ - pattern: $REQ.query.$FIELD
40
+ - pattern: $REQ.body.$FIELD
41
+ - pattern: $REQ.params.$FIELD
42
+ pattern-sinks:
43
+ - patterns:
44
+ - pattern: $DB.query($QUERY, ...)
45
+ - focus-metavariable: $QUERY
46
+ - id: pbai-tls-verification-disabled
47
+ languages: [javascript, typescript]
48
+ severity: WARNING
49
+ message: Verificacao do certificado TLS desativada.
50
+ pattern: "{..., rejectUnauthorized: false, ...}"