@pathbit/agentdevkit 0.0.10 → 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.
- package/.pbai4devs/COMPATIBILIDADE.md +6 -0
- package/.pbai4devs/INSTRUCTIONS.md +3 -0
- package/.pbai4devs/agents/README.md +19 -0
- package/.pbai4devs/agents/auditor-seguranca/AGENT.md +20 -0
- package/.pbai4devs/agents/auditor-seguranca/antigravity.yaml +8 -0
- package/.pbai4devs/agents/auditor-seguranca/claude.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/cursor.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/gemini.yaml +6 -0
- package/.pbai4devs/agents/auditor-seguranca/github.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/mimocode.yaml +8 -0
- package/.pbai4devs/agents/auditor-seguranca/opencode.yaml +11 -0
- package/.pbai4devs/agents/documentador/AGENT.md +19 -0
- package/.pbai4devs/agents/documentador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/documentador/claude.yaml +5 -0
- package/.pbai4devs/agents/documentador/cursor.yaml +5 -0
- package/.pbai4devs/agents/documentador/gemini.yaml +6 -0
- package/.pbai4devs/agents/documentador/github.yaml +5 -0
- package/.pbai4devs/agents/documentador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/documentador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/documentador/opencode.yaml +11 -0
- package/.pbai4devs/agents/implementador/AGENT.md +20 -0
- package/.pbai4devs/agents/implementador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/implementador/claude.yaml +5 -0
- package/.pbai4devs/agents/implementador/cursor.yaml +5 -0
- package/.pbai4devs/agents/implementador/gemini.yaml +6 -0
- package/.pbai4devs/agents/implementador/github.yaml +5 -0
- package/.pbai4devs/agents/implementador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/implementador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/implementador/opencode.yaml +11 -0
- package/.pbai4devs/agents/planejador/AGENT.md +19 -0
- package/.pbai4devs/agents/planejador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/planejador/claude.yaml +5 -0
- package/.pbai4devs/agents/planejador/cursor.yaml +5 -0
- package/.pbai4devs/agents/planejador/gemini.yaml +6 -0
- package/.pbai4devs/agents/planejador/github.yaml +5 -0
- package/.pbai4devs/agents/planejador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/planejador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/planejador/opencode.yaml +11 -0
- package/.pbai4devs/agents/validador/AGENT.md +19 -0
- package/.pbai4devs/agents/validador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/validador/claude.yaml +5 -0
- package/.pbai4devs/agents/validador/cursor.yaml +5 -0
- package/.pbai4devs/agents/validador/gemini.yaml +6 -0
- package/.pbai4devs/agents/validador/github.yaml +5 -0
- package/.pbai4devs/agents/validador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/validador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/validador/opencode.yaml +11 -0
- package/.pbai4devs/commands/README.md +13 -0
- package/.pbai4devs/commands/atualizar-documentacao/COMMAND.md +17 -0
- package/.pbai4devs/commands/auditar-seguranca/COMMAND.md +25 -0
- package/.pbai4devs/commands/implementar-entrega/COMMAND.md +20 -0
- package/.pbai4devs/commands/planejar-entrega/COMMAND.md +17 -0
- package/.pbai4devs/commands/preparar-seguranca/COMMAND.md +22 -0
- package/.pbai4devs/commands/validar-entrega/COMMAND.md +18 -0
- package/.pbai4devs/commands/verificar-dependencias/COMMAND.md +18 -0
- package/.pbai4devs/commands/verificar-segredos/COMMAND.md +19 -0
- package/.pbai4devs/external-skills/CATALOG.json +39 -1
- package/.pbai4devs/external-skills/README.md +13 -0
- package/.pbai4devs/rules/README.md +4 -0
- package/.pbai4devs/rules/execucao-local/RULE.md +20 -0
- package/.pbai4devs/rules/execucao-local/cursor.yaml +4 -0
- package/.pbai4devs/rules/seguranca/RULE.md +19 -0
- package/.pbai4devs/rules/seguranca/cursor.yaml +4 -0
- package/.pbai4devs/scripts/README.md +1 -0
- package/.pbai4devs/scripts/auditar-seguranca/README.md +57 -0
- package/.pbai4devs/scripts/auditar-seguranca/SCRIPT.mjs +411 -0
- package/.pbai4devs/scripts/auditar-seguranca/database-zip.mjs +143 -0
- package/.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml +50 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/SMOKE.mjs +74 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/TEST.mjs +247 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/zip-fixture.mjs +45 -0
- package/.pbai4devs/scripts/lib/native-release.mjs +193 -0
- package/.pbai4devs/scripts/preparar-ferramentas/README.md +8 -2
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +46 -16
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +2 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/native-release.test.mjs +103 -0
- package/.pbai4devs/skills/README.md +10 -2
- package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +5 -1
- package/.pbai4devs/skills/seguranca/REFERENCE.md +104 -0
- package/.pbai4devs/skills/seguranca/corrigir-vulnerabilidade/SKILL.md +27 -0
- package/.pbai4devs/skills/seguranca/modelar-ameacas/SKILL.md +25 -0
- package/.pbai4devs/skills/seguranca/revisar-autorizacao/SKILL.md +29 -0
- package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +27 -0
- package/.pbai4devs/templates/README.md +2 -0
- package/.pbai4devs/templates/revisao-seguranca/TEMPLATE.md +33 -0
- package/.pbai4devs/tools/README.md +11 -3
- package/.pbai4devs/tools/gitleaks/README.md +22 -0
- package/.pbai4devs/tools/gitleaks/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/gitleaks/TOOL.json +67 -0
- package/.pbai4devs/tools/opengrep/README.md +22 -0
- package/.pbai4devs/tools/opengrep/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/opengrep/TOOL.json +71 -0
- package/.pbai4devs/tools/osv-scanner/README.md +23 -0
- package/.pbai4devs/tools/osv-scanner/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/osv-scanner/TOOL.json +70 -0
- package/CHANGELOG.md +9 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +3 -2
- package/docs/README.md +6 -3
- package/docs/guides/README.md +1 -0
- package/docs/guides/installation.md +3 -3
- package/docs/guides/resources.md +48 -1
- package/docs/guides/security-references.md +97 -0
- package/docs/guides/security.md +32 -0
- package/docs/maintainers/maintenance.md +7 -0
- package/docs/maintainers/packages.md +2 -2
- package/docs/product/prd.md +4 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +1 -0
- package/docs/specs/security-ecosystem/README.md +9 -0
- package/docs/specs/security-ecosystem/SPEC.md +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { SecurityAudit } from '../SCRIPT.mjs';
|
|
7
|
+
import { ToolManager } from '../../preparar-ferramentas/SCRIPT.mjs';
|
|
8
|
+
import { zipFixture } from './zip-fixture.mjs';
|
|
9
|
+
|
|
10
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
11
|
+
const manager = new ToolManager({ root });
|
|
12
|
+
for (const name of ['gitleaks', 'osv-scanner', 'opengrep']) manager.check(name);
|
|
13
|
+
const base = join(root, '.agent-tests/security-smoke'); fs.mkdirSync(base, { recursive: true });
|
|
14
|
+
const work = fs.mkdtempSync(join(base, 'run-'));
|
|
15
|
+
const fixture = join(work, 'project'); fs.mkdirSync(fixture);
|
|
16
|
+
const write = (file, bytes) => { fs.mkdirSync(dirname(file), { recursive: true }); fs.writeFileSync(file, bytes); };
|
|
17
|
+
for (const file of ['scripts/preparar-ferramentas/SCRIPT.mjs', 'scripts/lib/native-release.mjs', 'scripts/lib/runtime.mjs', 'scripts/auditar-seguranca/rules/javascript.yml']) write(join(fixture, '.pbai4devs', file), fs.readFileSync(join(root, '.pbai4devs', file)));
|
|
18
|
+
for (const name of ['gitleaks', 'osv-scanner', 'opengrep']) {
|
|
19
|
+
const source = join(root, '.pbai4devs/tools', name), target = join(fixture, '.pbai4devs/tools', name);
|
|
20
|
+
for (const file of ['TOOL.json', 'SCRIPT.mjs']) write(join(target, file), fs.readFileSync(join(source, file)));
|
|
21
|
+
const current = JSON.parse(fs.readFileSync(join(source, '.local/CURRENT.json')));
|
|
22
|
+
const definition = JSON.parse(fs.readFileSync(join(source, 'TOOL.json')));
|
|
23
|
+
for (const path of Object.values(definition.envPaths || {})) fs.mkdirSync(join(target, '.local', path), { recursive: true });
|
|
24
|
+
write(join(target, '.local/CURRENT.json'), fs.readFileSync(join(source, '.local/CURRENT.json')));
|
|
25
|
+
const version = `${current.version}-${current.platform}`;
|
|
26
|
+
fs.cpSync(join(source, '.local/versions', version), join(target, '.local/versions', version), { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
const syntheticSecret = 'ghp_' + 'a2f80b357bba4272fc7bb1382e82cdb36045';
|
|
29
|
+
write(join(fixture, 'app/index.js'), `import { exec } from 'node:child_process';
|
|
30
|
+
function unsafe(req, db) {
|
|
31
|
+
eval(req.query.code);
|
|
32
|
+
exec(req.body.command);
|
|
33
|
+
db.query('select * from users where id=' + req.params.id);
|
|
34
|
+
const tls = { rejectUnauthorized: false };
|
|
35
|
+
return tls;
|
|
36
|
+
}
|
|
37
|
+
const apiKey = '${syntheticSecret}';
|
|
38
|
+
`);
|
|
39
|
+
const lock = version => JSON.stringify({ name: 'fixture', version: '1.0.0', lockfileVersion: 3, packages: { '': { name: 'fixture', version: '1.0.0', dependencies: { 'security-fixture-lib': version } }, 'node_modules/security-fixture-lib': { version, resolved: `https://registry.npmjs.org/security-fixture-lib/-/security-fixture-lib-${version}.tgz` } } });
|
|
40
|
+
write(join(fixture, 'app/package-lock.json'), lock('1.0.0'));
|
|
41
|
+
const audit = new SecurityAudit({ root: fixture });
|
|
42
|
+
const vulnerability = { schema_version: '1.6.0', id: 'TEST-2099-0001', modified: '2025-01-01T00:00:00Z', published: '2025-01-01T00:00:00Z', summary: 'Synthetic vulnerability used only by local tests', affected: [{ package: { ecosystem: 'npm', name: 'security-fixture-lib' }, ranges: [{ type: 'SEMVER', events: [{ introduced: '0' }, { fixed: '1.0.1' }] }], versions: ['1.0.0'] }] };
|
|
43
|
+
const archive = zipFixture({ 'TEST-2099-0001.json': JSON.stringify(vulnerability) });
|
|
44
|
+
audit.fetcher = async () => new Response(archive);
|
|
45
|
+
await audit.updateData(['npm']);
|
|
46
|
+
const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
47
|
+
const before = digest(join(fixture, 'app/index.js'));
|
|
48
|
+
const bad = await audit.run({ target: 'app' });
|
|
49
|
+
write(join(work, 'unsafe-report.json'), JSON.stringify(bad, null, 2));
|
|
50
|
+
assert.equal(bad.status, 'findings', JSON.stringify(bad.checks));
|
|
51
|
+
for (const check of bad.checks) assert.equal(check.status, 'findings', check.tool);
|
|
52
|
+
assert.equal(digest(join(fixture, 'app/index.js')), before);
|
|
53
|
+
assert.ok(!JSON.stringify(bad).includes(syntheticSecret));
|
|
54
|
+
assert.ok(bad.findings.some(finding => finding.rule === 'TEST-2099-0001'));
|
|
55
|
+
for (const id of ['pbai-eval-input', 'pbai-shell-input', 'pbai-sql-input', 'pbai-tls-verification-disabled']) assert.ok(bad.findings.some(finding => finding.rule === id), id);
|
|
56
|
+
write(join(fixture, 'app/index.js'), `import { execFile } from 'node:child_process';
|
|
57
|
+
function safe(req, db) {
|
|
58
|
+
JSON.parse(req.query.data);
|
|
59
|
+
execFile('printf', ['%s', req.body.message]);
|
|
60
|
+
db.query('select * from users where id = ?', [req.params.id]);
|
|
61
|
+
return { rejectUnauthorized: true };
|
|
62
|
+
}
|
|
63
|
+
`);
|
|
64
|
+
write(join(fixture, 'app/package-lock.json'), lock('1.0.1'));
|
|
65
|
+
const good = await audit.run({ target: 'app' });
|
|
66
|
+
write(join(work, 'safe-report.json'), JSON.stringify(good, null, 2));
|
|
67
|
+
assert.equal(good.status, 'passed', JSON.stringify(good.checks)); assert.equal(good.findings.length, 0);
|
|
68
|
+
write(join(fixture, 'app/package-lock.json'), JSON.stringify({ name: 'fixture', version: '1.0.0', lockfileVersion: 3, packages: { '': { name: 'fixture', version: '1.0.0' } } }));
|
|
69
|
+
const empty = await audit.run({ target: 'app', checks: ['dependencies'] });
|
|
70
|
+
write(join(work, 'no-dependencies-report.json'), JSON.stringify(empty, null, 2));
|
|
71
|
+
assert.equal(empty.status, 'not-applicable', JSON.stringify(empty.checks));
|
|
72
|
+
const report = { status: 'passed', work, tools: bad.checks.map(item => ({ tool: item.tool, version: item.version })), positiveFindings: bad.findings.length, negativeFindings: good.findings.length, noDependencies: empty.status, offline: true, sourcePreserved: true };
|
|
73
|
+
write(join(work, 'REPORT.json'), JSON.stringify(report, null, 2));
|
|
74
|
+
console.log(JSON.stringify(report));
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { SecurityAudit, parseArgs, parseGitleaks, parseOsv, parseOpengrep, sanitizedEnvironment, safePath, snapshotSources, verifyDatabase } from '../SCRIPT.mjs';
|
|
8
|
+
import { zipFixture } from './zip-fixture.mjs';
|
|
9
|
+
import { validateOsvDatabase } from '../database-zip.mjs';
|
|
10
|
+
|
|
11
|
+
const write = (path, text) => { fs.mkdirSync(dirname(path), { recursive: true }); fs.writeFileSync(path, text); };
|
|
12
|
+
const toolPath = args => args[0].replaceAll('\\', '/');
|
|
13
|
+
const advisory = (id = 'TEST-1', extra = {}) => JSON.stringify({ id, modified: '2026-01-01T00:00:00Z', affected: [], ...extra });
|
|
14
|
+
const validZip = () => zipFixture({ 'TEST-1.json': advisory() });
|
|
15
|
+
function fixture(t) {
|
|
16
|
+
const root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'pbai-security-')));
|
|
17
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
18
|
+
assert.equal(spawnSync('git', ['init', '--quiet', root]).status, 0);
|
|
19
|
+
write(join(root, '.gitignore'), '/.agent-tests/\n');
|
|
20
|
+
write(join(root, 'app/index.js'), 'export const answer = 42;\n');
|
|
21
|
+
write(join(root, '.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml'), 'rules: []\n');
|
|
22
|
+
for (const name of ['gitleaks', 'osv-scanner', 'opengrep']) write(join(root, `.pbai4devs/tools/${name}/SCRIPT.mjs`), '// fixture\n');
|
|
23
|
+
const manager = { root, check: () => ({ version: '1.0.0' }), entry: () => ({ envPaths: { OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY: 'data/osv' } }), local: () => join(root, '.pbai4devs/tools/osv-scanner/.local'), withLock: (_name, action) => action() };
|
|
24
|
+
const calls = [];
|
|
25
|
+
const execute = (_node, args, options) => {
|
|
26
|
+
calls.push({ args, options });
|
|
27
|
+
const payload = toolPath(args).includes('/gitleaks/') ? [] : toolPath(args).includes('/osv-scanner/') ? { results: [] }
|
|
28
|
+
: { results: [], errors: [], paths: { scanned: [join(options.cwd, 'source/index.js')] } };
|
|
29
|
+
const output = args.find(arg => arg.startsWith('--report-path=') || arg.startsWith('--output-file='));
|
|
30
|
+
if (output) fs.writeFileSync(output.slice(output.indexOf('=') + 1), JSON.stringify(payload));
|
|
31
|
+
return { status: 0, stdout: JSON.stringify(payload), stderr: '' };
|
|
32
|
+
};
|
|
33
|
+
return { root, manager, calls, execute, audit: new SecurityAudit({ root, manager, execute }) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('argumentos limitam checks, destinos e operacoes', () => {
|
|
37
|
+
assert.equal(parseArgs([]).command, 'help');
|
|
38
|
+
assert.deepEqual(parseArgs(['run', '--checks', 'sast,secrets,secrets']).checks, ['sast', 'secrets']);
|
|
39
|
+
assert.deepEqual(parseArgs(['update-data', '--ecosystems', 'NPM,go']).ecosystems, ['npm', 'Go']);
|
|
40
|
+
for (const args of [['scan'], ['run', '--checks', 'cloud'], ['run', '--checks', ''], ['check', '--target', '.'], ['run', '--ecosystems', 'npm'], ['update-data'], ['update-data', '--ecosystems', '../../etc'], ['run', '--target', '.', '--target', '.'], ['run', '--checks', 'sast', '--include-history']]) assert.throws(() => parseArgs(args));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('ambiente nao encaminha tokens, proxies, Node options ou configuracoes externas', () => {
|
|
44
|
+
const env = sanitizedEnvironment({ PATH: '/bin', HOME: '/home/person', SystemRoot: 'C:\\Windows', AWS_ACCESS_KEY_ID: 'secret', GOOGLE_APPLICATION_CREDENTIALS: 'secret', SECRET: 'secret', NPM_TOKEN: 'secret', NODE_OPTIONS: '--require malicious', HTTPS_PROXY: 'secret', SEMGREP_RULES: 'https://remote', GITLEAKS_CONFIG: 'secret' });
|
|
45
|
+
assert.equal(env.PATH, '/bin'); assert.equal(env.HOME, '/home/person'); assert.equal(env.SystemRoot, 'C:\\Windows');
|
|
46
|
+
assert.ok(!JSON.stringify(env).includes('secret')); assert.equal(env.NODE_OPTIONS, undefined); assert.equal(env.SEMGREP_RULES, undefined);
|
|
47
|
+
assert.equal(env.OPENGREP_ENABLE_VERSION_CHECK, '0');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('caminhos recusam escapes, links e output rastreado antes de executar', async t => {
|
|
51
|
+
const f = fixture(t); const outside = fs.mkdtempSync(join(tmpdir(), 'pbai-outside-')); t.after(() => fs.rmSync(outside, { recursive: true, force: true }));
|
|
52
|
+
fs.symlinkSync(outside, join(f.root, 'link'), process.platform === 'win32' ? 'junction' : undefined);
|
|
53
|
+
assert.throws(() => safePath(f.root, '../outside')); assert.throws(() => safePath(f.root, 'link/file.json'));
|
|
54
|
+
await assert.rejects(f.audit.run({ target: 'app', checks: ['secrets'], output: 'report.json' }));
|
|
55
|
+
fs.mkdirSync(join(f.root, '.agent-tests/security'), { recursive: true });
|
|
56
|
+
await assert.rejects(f.audit.run({ target: '.agent-tests', checks: ['secrets'] }), /conflita/);
|
|
57
|
+
await assert.rejects(f.audit.run({ target: '.agent-tests/security', checks: ['secrets'] }), /conflita/);
|
|
58
|
+
write(join(f.root, '.agent-tests/tracked.json'), '{}');
|
|
59
|
+
assert.equal(spawnSync('git', ['-C', f.root, 'add', '-f', '.agent-tests/tracked.json']).status, 0);
|
|
60
|
+
await assert.rejects(f.audit.run({ target: 'app', checks: ['secrets'], output: '.agent-tests/tracked.json' }));
|
|
61
|
+
assert.equal(f.calls.length, 0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('snapshot preserva originais e nao atravessa links nem diretorios gerados', t => {
|
|
65
|
+
const f = fixture(t); write(join(f.root, 'app/node_modules/excluded.js'), 'secret'); write(join(f.root, 'app/.semgrepignore'), '*');
|
|
66
|
+
fs.symlinkSync(join(f.root, 'app'), join(f.root, 'app/link'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
67
|
+
const destination = join(f.root, '.agent-tests/snapshot'); fs.mkdirSync(destination, { recursive: true });
|
|
68
|
+
const data = snapshotSources(f.root, join(f.root, 'app'), destination);
|
|
69
|
+
assert.deepEqual(data.files.map(file => file.path), ['index.js']);
|
|
70
|
+
assert.equal(data.excluded.length, 3); assert.equal(fs.readFileSync(join(f.root, 'app/index.js'), 'utf8'), fs.readFileSync(join(destination, 'index.js'), 'utf8'));
|
|
71
|
+
assert.throws(() => snapshotSources(f.root, join(f.root, 'app'), destination, { maxFileBytes: 1 }));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('parsers redigem segredos e recusam saidas invalidas ou falsas aprovacoes', () => {
|
|
75
|
+
const secret = 'a-secret-value-that-must-not-escape';
|
|
76
|
+
const gitleaks = parseGitleaks([{ RuleID: 'key', File: 'a.js', StartLine: 3, Secret: secret, Match: secret, Line: secret, Description: secret }], 1, '/tmp');
|
|
77
|
+
assert.equal(gitleaks.status, 'findings'); assert.ok(!JSON.stringify(gitleaks).includes(secret));
|
|
78
|
+
assert.throws(() => parseGitleaks([], 1, '/tmp')); assert.throws(() => parseGitleaks([], 2, '/tmp')); assert.throws(() => parseGitleaks([{ RuleID: 'x', File: '../escape' }], 1, '/tmp'));
|
|
79
|
+
assert.throws(() => parseGitleaks([{ RuleID: 'x', File: 'src/../../escape' }], 1, '/tmp'));
|
|
80
|
+
assert.equal(parseOsv({ results: [] }, 128, '/tmp').status, 'not-applicable'); assert.throws(() => parseOsv({ results: [] }, 127, '/tmp')); assert.throws(() => parseOsv({ results: [] }, 1, '/tmp'));
|
|
81
|
+
const osv = parseOsv({ results: [{ source: { path: 'package-lock.json' }, packages: [{ package: { name: 'demo', version: '1.0.0', ecosystem: 'npm' }, vulnerabilities: [{ id: 'TEST-1', details: secret }] }] }] }, 1, '/tmp');
|
|
82
|
+
assert.equal(osv.findings.length, 1); assert.ok(!JSON.stringify(osv).includes(secret));
|
|
83
|
+
const sast = parseOpengrep({ results: [{ check_id: 'pbai-eval-input', path: 'a.js', start: { line: 2 }, extra: { lines: secret, message: secret } }], errors: [], paths: { scanned: ['a.js'] } }, 1, '/tmp');
|
|
84
|
+
assert.equal(sast.findings.length, 1); assert.ok(!JSON.stringify(sast).includes(secret));
|
|
85
|
+
assert.throws(() => parseOpengrep({ results: [], errors: [{}], paths: { scanned: [] } }, 0, '/tmp'));
|
|
86
|
+
assert.throws(() => parseOpengrep({ results: [], errors: [] }, 0, '/tmp'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Opengrep usa IDs e fingerprints estaveis sem caminhos de Linux, Windows ou macOS', () => {
|
|
90
|
+
const rules = ['pbai-eval-input', 'pbai-shell-input', 'pbai-sql-input', 'pbai-tls-verification-disabled'];
|
|
91
|
+
const prefixes = ['', 'home.runner.work.AgentDevKit.AgentDevKit..agent-tests.security-smoke.run-eu1Fdm..pbai4devs.scripts.auditar-seguranca.rules.',
|
|
92
|
+
'D.a.AgentDevKit.AgentDevKit..agent-tests.security-smoke.run-TpubGc..pbai4devs.scripts.auditar-seguranca.rules.',
|
|
93
|
+
'Users.person.Library.Project..agent-tests.security-smoke.run-macABC..pbai4devs.scripts.auditar-seguranca.rules.'];
|
|
94
|
+
const reports = prefixes.map(prefix => parseOpengrep({
|
|
95
|
+
results: rules.map((rule, index) => ({ check_id: `${prefix}${rule}`, path: 'src/handler.js', start: { line: index + 1 }, extra: { severity: 'ERROR' } })),
|
|
96
|
+
errors: [], paths: { scanned: ['src/handler.js'] },
|
|
97
|
+
}, 1, '/unused'));
|
|
98
|
+
for (const report of reports) {
|
|
99
|
+
assert.deepEqual(report, reports[0]);
|
|
100
|
+
assert.deepEqual(report.findings.map(item => item.rule), rules);
|
|
101
|
+
assert.equal(new Set(report.findings.map(item => item.fingerprint)).size, rules.length);
|
|
102
|
+
assert.doesNotMatch(JSON.stringify(report), /runner|person|run-eu1Fdm|run-TpubGc|run-macABC|AgentDevKit|pbai4devs|D\.a\./);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('Opengrep rejeita regras desconhecidas e semelhantes sem divulgar o ID bruto', () => {
|
|
107
|
+
const unknown = ['unknown', 'pbai-eval-input-extra', 'prefix.pbai-eval-input.extra', 'prefix.pbai-unknown', '/private/person/pbai-eval-input', ''];
|
|
108
|
+
for (const check_id of unknown) {
|
|
109
|
+
assert.throws(() => parseOpengrep({ results: [{ check_id, path: 'src/handler.js', start: { line: 1 } }], errors: [], paths: { scanned: ['src/handler.js'] } }, 1, '/unused'),
|
|
110
|
+
error => error.message === 'Identificador de regra Opengrep desconhecido.' && error.publicMessage === error.message);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('regra Opengrep desconhecida torna a auditoria incompleta sem vazar caminho local', async t => {
|
|
115
|
+
const f = fixture(t), check_id = 'Users.private-person.project.run-private.rules.unrecognized';
|
|
116
|
+
f.audit.execute = () => ({ status: 1, stdout: JSON.stringify({ results: [{ check_id, path: 'index.js', start: { line: 1 } }], errors: [], paths: { scanned: ['index.js'] } }), stderr: '' });
|
|
117
|
+
const report = await f.audit.run({ target: 'app', checks: ['sast'] });
|
|
118
|
+
assert.equal(report.status, 'incomplete'); assert.equal(report.exitCode, 2);
|
|
119
|
+
assert.deepEqual(report.findings, []); assert.equal(report.checks[0].status, 'not-run');
|
|
120
|
+
assert.doesNotMatch(JSON.stringify(report), /private-person|run-private|unrecognized/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('run usa argumentos locais e fecha incompleto quando ferramenta ou DB falta', async t => {
|
|
124
|
+
const f = fixture(t);
|
|
125
|
+
const result = await f.audit.run({ target: 'app', checks: ['secrets', 'sast'] });
|
|
126
|
+
assert.equal(result.status, 'passed'); assert.equal(result.exitCode, 0);
|
|
127
|
+
assert.ok(f.calls.every(call => call.options.shell === false && call.options.timeout > 0));
|
|
128
|
+
assert.ok(f.calls.some(call => call.args.includes('--disable-version-check') && call.args.includes('--no-git-ignore')));
|
|
129
|
+
assert.ok(f.calls.some(call => call.args.includes('--redact=100')));
|
|
130
|
+
assert.ok(!f.calls.some(call => call.args.some(arg => /metrics|https:\/\/|--autofix/.test(arg))));
|
|
131
|
+
assert.ok(fs.existsSync(join(f.root, result.report))); assert.ok(!fs.readdirSync(join(f.root, '.agent-tests/security')).some(name => name.startsWith('.work-')));
|
|
132
|
+
write(join(f.root, 'app/package-lock.json'), '{}');
|
|
133
|
+
const missingDb = await f.audit.run({ target: 'app', checks: ['dependencies'] });
|
|
134
|
+
assert.equal(missingDb.status, 'incomplete'); assert.equal(missingDb.exitCode, 2);
|
|
135
|
+
f.manager.check = () => { throw Error('Missing TOKEN=secret'); };
|
|
136
|
+
const missingTool = await f.audit.run({ target: 'app', checks: ['secrets'] });
|
|
137
|
+
assert.equal(missingTool.status, 'incomplete'); assert.ok(!JSON.stringify(missingTool).includes('TOKEN'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('alteracao da fonte durante a ferramenta invalida o resultado', async t => {
|
|
141
|
+
const f = fixture(t), execute = f.execute;
|
|
142
|
+
f.audit.execute = (...args) => { const result = execute(...args); write(join(f.root, 'app/index.js'), 'changed'); return result; };
|
|
143
|
+
const result = await f.audit.run({ target: 'app', checks: ['secrets'] });
|
|
144
|
+
assert.equal(result.status, 'incomplete'); assert.equal(result.exitCode, 2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('saida quebrada da ferramenta nao vira relatorio limpo', async t => {
|
|
148
|
+
const f = fixture(t); f.audit.execute = () => ({ status: 2, stdout: 'secret', stderr: 'secret' });
|
|
149
|
+
const result = await f.audit.run({ target: 'app', checks: ['sast'] });
|
|
150
|
+
assert.equal(result.exitCode, 2); assert.ok(!JSON.stringify(result).includes('secret'));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('bases OSV atualizam explicitamente, validam hash e preservam anterior em falha', async t => {
|
|
154
|
+
const f = fixture(t); const zip = validZip(); let calls = 0;
|
|
155
|
+
f.audit.fetcher = async (url, options) => { calls++; assert.equal(url, 'https://osv-vulnerabilities.storage.googleapis.com/npm/all.zip'); assert.equal(options.redirect, 'error'); assert.equal(options.credentials, 'omit'); assert.deepEqual(Object.keys(options.headers), ['Accept']); return new Response(zip); };
|
|
156
|
+
assert.equal((await f.audit.updateData(['npm'])).status, 'passed');
|
|
157
|
+
const cache = join(f.manager.local(), 'data/osv'); const before = verifyDatabase(f.root, cache, 'npm'); assert.equal(calls, 1);
|
|
158
|
+
f.audit.fetcher = async () => new Response('broken'); await assert.rejects(f.audit.updateData(['npm']));
|
|
159
|
+
assert.deepEqual(verifyDatabase(f.root, cache, 'npm'), before);
|
|
160
|
+
const fake = Buffer.alloc(22); fake.writeUInt32LE(0x04034b50); fake.writeUInt32LE(0x06054b50, 4);
|
|
161
|
+
f.audit.fetcher = async () => new Response(fake); await assert.rejects(f.audit.updateData(['npm']));
|
|
162
|
+
assert.deepEqual(verifyDatabase(f.root, cache, 'npm'), before);
|
|
163
|
+
f.audit.fetcher = async () => new Response(zipFixture({ 'TEST-1.json': '{}' })); await assert.rejects(f.audit.updateData(['npm']));
|
|
164
|
+
assert.deepEqual(verifyDatabase(f.root, cache, 'npm'), before);
|
|
165
|
+
fs.appendFileSync(join(cache, 'osv-scalibr/npm/all.zip'), 'tampered'); assert.throws(() => verifyDatabase(f.root, cache, 'npm'));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('query check nao escreve relatorios nem instala ferramentas', t => {
|
|
169
|
+
const f = fixture(t); const before = fs.readdirSync(f.root);
|
|
170
|
+
const result = f.audit.check(['secrets', 'sast']); assert.equal(result.exitCode, 0); assert.deepEqual(fs.readdirSync(f.root), before); assert.equal(f.calls.length, 0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('historico nunca amplia silenciosamente um target de subpasta', async t => {
|
|
174
|
+
const f = fixture(t);
|
|
175
|
+
await assert.rejects(f.audit.run({ target: 'app', checks: ['secrets'], includeHistory: true }), /raiz do repositorio/);
|
|
176
|
+
assert.equal(f.calls.length, 0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('bundles de texto continuam incluidos no snapshot de segredos', t => {
|
|
180
|
+
const f = fixture(t); write(join(f.root, 'app/dist/app.js'), 'bundle'); write(join(f.root, 'app/build/server.js'), 'server');
|
|
181
|
+
const destination = join(f.root, '.agent-tests/snapshot'); fs.mkdirSync(destination, { recursive: true });
|
|
182
|
+
const data = snapshotSources(f.root, join(f.root, 'app'), destination);
|
|
183
|
+
assert.ok(data.files.some(file => file.path === 'dist/app.js')); assert.ok(data.files.some(file => file.path === 'build/server.js'));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('OSV recebe modo offline estrito e erro de outro scanner prevalece sobre achados', async t => {
|
|
187
|
+
const f = fixture(t); const zip = validZip(); f.audit.fetcher = async () => new Response(zip); await f.audit.updateData(['npm']);
|
|
188
|
+
write(join(f.root, 'app/package-lock.json'), '{}');
|
|
189
|
+
const execute = f.execute;
|
|
190
|
+
f.audit.execute = (node, args, options) => {
|
|
191
|
+
if (toolPath(args).includes('/osv-scanner/')) {
|
|
192
|
+
assert.ok(args.includes('--offline')); assert.ok(args.includes('--no-resolve')); assert.ok(args.includes('--all-packages'));
|
|
193
|
+
assert.ok(!args.some(arg => arg.includes('call-analysis') || arg.includes('licenses')));
|
|
194
|
+
}
|
|
195
|
+
return execute(node, args, options);
|
|
196
|
+
};
|
|
197
|
+
const result = await f.audit.run({ target: 'app', checks: ['dependencies'] }); assert.equal(result.status, 'not-applicable');
|
|
198
|
+
f.audit.execute = (_node, args) => {
|
|
199
|
+
if (toolPath(args).includes('/gitleaks/')) { const output = args.find(arg => arg.startsWith('--report-path=')); fs.writeFileSync(output.slice(output.indexOf('=') + 1), JSON.stringify([{ RuleID: 'key', File: 'index.js', StartLine: 1 }])); return { status: 1, stdout: '', stderr: '' }; }
|
|
200
|
+
return { status: 2, stdout: '{}', stderr: 'failure' };
|
|
201
|
+
};
|
|
202
|
+
const mixed = await f.audit.run({ target: 'app', checks: ['secrets', 'sast'] }); assert.equal(mixed.exitCode, 2); assert.equal(mixed.findings.length, 1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('OSV sem pacotes usa 128 explicito; erro sem JSON permanece incompleto', async t => {
|
|
206
|
+
const f = fixture(t); const zip = validZip(); f.audit.fetcher = async () => new Response(zip); await f.audit.updateData(['npm']);
|
|
207
|
+
write(join(f.root, 'app/package-lock.json'), '{}');
|
|
208
|
+
f.audit.execute = () => ({ status: 128, stdout: '', stderr: '' });
|
|
209
|
+
assert.equal((await f.audit.run({ target: 'app', checks: ['dependencies'] })).status, 'not-applicable');
|
|
210
|
+
f.audit.execute = () => ({ status: 127, stdout: '', stderr: '' });
|
|
211
|
+
assert.equal((await f.audit.run({ target: 'app', checks: ['dependencies'] })).status, 'incomplete');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('ZIP e ZIP64 exigem estrutura, CRC, JSON OSV e limites validos', () => {
|
|
215
|
+
for (const options of [{}, { stored: true }, { descriptor: true }, { zip64: true }, { zip64: true, descriptor: true }]) {
|
|
216
|
+
const zip = zipFixture({ 'TEST-1.json': advisory(), 'TEST-2.json': advisory('TEST-2', { withdrawn: '2026-02-01T00:00:00Z', affected: undefined }) }, options);
|
|
217
|
+
assert.equal(validateOsvDatabase(zip).records, 2);
|
|
218
|
+
}
|
|
219
|
+
const zero = Buffer.alloc(22); zero.writeUInt32LE(0x04034b50); zero.writeUInt32LE(0x06054b50, 4);
|
|
220
|
+
assert.throws(() => validateOsvDatabase(zero));
|
|
221
|
+
assert.throws(() => validateOsvDatabase(zipFixture({ 'TEST-1.json': 'invalid' })));
|
|
222
|
+
assert.throws(() => validateOsvDatabase(zipFixture({ 'TEST-1.json': '{}' })));
|
|
223
|
+
assert.throws(() => validateOsvDatabase(zipFixture({ '../TEST-1.json': advisory() })));
|
|
224
|
+
assert.throws(() => validateOsvDatabase(zipFixture([['TEST-1.json', advisory()], ['TEST-1.json', advisory()]])));
|
|
225
|
+
const brokenCrc = validZip(); const central = brokenCrc.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
|
|
226
|
+
brokenCrc.writeUInt32LE(1, central + 16); assert.throws(() => validateOsvDatabase(brokenCrc));
|
|
227
|
+
const brokenPayload = validZip(); const payload = 30 + brokenPayload.readUInt16LE(26) + brokenPayload.readUInt16LE(28); brokenPayload[payload] ^= 0xff;
|
|
228
|
+
assert.throws(() => validateOsvDatabase(brokenPayload));
|
|
229
|
+
assert.throws(() => validateOsvDatabase(validZip(), { maxEntryBytes: 2 }));
|
|
230
|
+
assert.throws(() => validateOsvDatabase(validZip(), { maxTotalBytes: 2 }));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('inventario final recusa adicao, remocao, renomeacao e mudanca de tipo', async t => {
|
|
234
|
+
const changes = {
|
|
235
|
+
addition: root => write(join(root, 'app/added-during-scan.js'), 'new'),
|
|
236
|
+
removal: root => fs.unlinkSync(join(root, 'app/index.js')),
|
|
237
|
+
rename: root => fs.renameSync(join(root, 'app/index.js'), join(root, 'app/renamed.js')),
|
|
238
|
+
type: root => { fs.unlinkSync(join(root, 'app/index.js')); fs.mkdirSync(join(root, 'app/index.js')); },
|
|
239
|
+
directory: root => fs.mkdirSync(join(root, 'app/new-folder')),
|
|
240
|
+
};
|
|
241
|
+
for (const [name, change] of Object.entries(changes)) await t.test(name, async subtest => {
|
|
242
|
+
const f = fixture(subtest), execute = f.execute;
|
|
243
|
+
f.audit.execute = (...args) => { const result = execute(...args); change(f.root); return result; };
|
|
244
|
+
const result = await f.audit.run({ target: 'app', checks: ['secrets'] });
|
|
245
|
+
assert.equal(result.status, 'incomplete'); assert.equal(result.exitCode, 2);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { deflateRawSync } from 'node:zlib';
|
|
2
|
+
|
|
3
|
+
const crc32 = bytes => {
|
|
4
|
+
let crc = 0xffffffff;
|
|
5
|
+
for (const byte of bytes) {
|
|
6
|
+
crc ^= byte;
|
|
7
|
+
for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
8
|
+
}
|
|
9
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Minimal deterministic ZIP builder for synthetic OSV database fixtures.
|
|
13
|
+
export function zipFixture(entries, { zip64 = false, descriptor = false, stored = false } = {}) {
|
|
14
|
+
const files = [], index = []; let offset = 0;
|
|
15
|
+
const rows = Array.isArray(entries) ? entries : Object.entries(entries);
|
|
16
|
+
for (const [name, text] of rows) {
|
|
17
|
+
const filename = Buffer.from(name), bytes = Buffer.from(text), zipped = stored ? bytes : deflateRawSync(bytes), crc = crc32(bytes);
|
|
18
|
+
const localExtra = Buffer.alloc(zip64 ? 20 : 0), centralExtra = Buffer.alloc(zip64 ? 28 : 0);
|
|
19
|
+
if (zip64) {
|
|
20
|
+
localExtra.writeUInt16LE(1); localExtra.writeUInt16LE(16, 2); localExtra.writeBigUInt64LE(BigInt(bytes.length), 4); localExtra.writeBigUInt64LE(BigInt(zipped.length), 12);
|
|
21
|
+
centralExtra.writeUInt16LE(1); centralExtra.writeUInt16LE(24, 2); centralExtra.writeBigUInt64LE(BigInt(bytes.length), 4); centralExtra.writeBigUInt64LE(BigInt(zipped.length), 12); centralExtra.writeBigUInt64LE(BigInt(offset), 20);
|
|
22
|
+
}
|
|
23
|
+
const trailer = Buffer.alloc(descriptor ? zip64 ? 24 : 16 : 0);
|
|
24
|
+
if (descriptor) {
|
|
25
|
+
trailer.writeUInt32LE(0x08074b50); trailer.writeUInt32LE(crc, 4);
|
|
26
|
+
if (zip64) { trailer.writeBigUInt64LE(BigInt(zipped.length), 8); trailer.writeBigUInt64LE(BigInt(bytes.length), 16); }
|
|
27
|
+
else { trailer.writeUInt32LE(zipped.length, 8); trailer.writeUInt32LE(bytes.length, 12); }
|
|
28
|
+
}
|
|
29
|
+
const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50); local.writeUInt16LE(zip64 ? 45 : 20, 4); local.writeUInt16LE(descriptor ? 8 : 0, 6); local.writeUInt16LE(stored ? 0 : 8, 8);
|
|
30
|
+
local.writeUInt32LE(descriptor ? 0 : crc, 14); local.writeUInt32LE(zip64 ? 0xffffffff : descriptor ? 0 : zipped.length, 18); local.writeUInt32LE(zip64 ? 0xffffffff : descriptor ? 0 : bytes.length, 22); local.writeUInt16LE(filename.length, 26); local.writeUInt16LE(localExtra.length, 28);
|
|
31
|
+
files.push(local, filename, localExtra, zipped, trailer);
|
|
32
|
+
const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50); central.writeUInt16LE(zip64 ? 45 : 20, 4); central.writeUInt16LE(zip64 ? 45 : 20, 6); central.writeUInt16LE(descriptor ? 8 : 0, 8); central.writeUInt16LE(stored ? 0 : 8, 10);
|
|
33
|
+
central.writeUInt32LE(crc, 16); central.writeUInt32LE(zip64 ? 0xffffffff : zipped.length, 20); central.writeUInt32LE(zip64 ? 0xffffffff : bytes.length, 24); central.writeUInt16LE(filename.length, 28); central.writeUInt16LE(centralExtra.length, 30); central.writeUInt32LE(zip64 ? 0xffffffff : offset, 42);
|
|
34
|
+
index.push(central, filename, centralExtra); offset += local.length + filename.length + localExtra.length + zipped.length + trailer.length;
|
|
35
|
+
}
|
|
36
|
+
const table = Buffer.concat(index), end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50);
|
|
37
|
+
end.writeUInt16LE(zip64 ? 0xffff : rows.length, 8); end.writeUInt16LE(zip64 ? 0xffff : rows.length, 10); end.writeUInt32LE(zip64 ? 0xffffffff : table.length, 12); end.writeUInt32LE(zip64 ? 0xffffffff : offset, 16);
|
|
38
|
+
const tail = [];
|
|
39
|
+
if (zip64) {
|
|
40
|
+
const wide = Buffer.alloc(56), locator = Buffer.alloc(20); wide.writeUInt32LE(0x06064b50); wide.writeBigUInt64LE(44n, 4); wide.writeUInt16LE(45, 12); wide.writeUInt16LE(45, 14);
|
|
41
|
+
wide.writeBigUInt64LE(BigInt(rows.length), 24); wide.writeBigUInt64LE(BigInt(rows.length), 32); wide.writeBigUInt64LE(BigInt(table.length), 40); wide.writeBigUInt64LE(BigInt(offset), 48);
|
|
42
|
+
locator.writeUInt32LE(0x07064b50); locator.writeBigUInt64LE(BigInt(offset + table.length), 8); locator.writeUInt32LE(1, 16); tail.push(wide, locator);
|
|
43
|
+
}
|
|
44
|
+
return Buffer.concat([...files, table, ...tail, end]);
|
|
45
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { gunzipSync, inflateRawSync } from 'node:zlib';
|
|
5
|
+
|
|
6
|
+
const limit = 256 * 1024 * 1024;
|
|
7
|
+
const hosts = new Set(['github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com', 'github-releases.githubusercontent.com']);
|
|
8
|
+
const fail = message => { throw new Error(message); };
|
|
9
|
+
export const nativeHash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
|
|
11
|
+
function safeName(value, directory = false) {
|
|
12
|
+
if (typeof value !== 'string' || !value || /[\\\x00-\x1f:]/.test(value) || isAbsolute(value)) fail('Caminho invalido no arquivo nativo.');
|
|
13
|
+
const normalized = directory ? value.replace(/\/$/, '') : value;
|
|
14
|
+
if (!normalized || normalized.split('/').some(part => !part || part === '.' || part === '..')) fail('Caminho invalido no arquivo nativo.');
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateNativeDownload(download, platforms) {
|
|
19
|
+
if (download?.provider !== 'github-release' || download.channel !== 'reviewed'
|
|
20
|
+
|| !/^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*$/.test(download.repository || '')
|
|
21
|
+
|| !/^\d+\.\d+\.\d+$/.test(download.version || '') || download.tag !== `v${download.version}`) fail('Release nativa deve usar origem e versao revisadas.');
|
|
22
|
+
if (!download.assets || typeof download.assets !== 'object' || Array.isArray(download.assets)
|
|
23
|
+
|| Object.keys(download.assets).length !== platforms.length) fail('Assets nativos devem corresponder as plataformas.');
|
|
24
|
+
for (const platform of platforms) {
|
|
25
|
+
const asset = download.assets[platform];
|
|
26
|
+
if (!Object.hasOwn(download.assets, platform) || !asset || typeof asset !== 'object' || !['raw', 'tar.gz', 'zip'].includes(asset.format)
|
|
27
|
+
|| !/^[a-f0-9]{64}$/.test(asset.sha256 || '') || !/^\w[\w.-]*$/.test(asset.file || '')) fail(`Asset nativo invalido: ${platform}`);
|
|
28
|
+
safeName(asset.binary);
|
|
29
|
+
}
|
|
30
|
+
return download;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function nativeRelease(entry, platform) {
|
|
34
|
+
validateNativeDownload(entry.download, entry.platforms);
|
|
35
|
+
const asset = entry.download.assets[platform];
|
|
36
|
+
if (!asset) fail(`Plataforma nativa nao suportada: ${platform}`);
|
|
37
|
+
const { repository, version, tag } = entry.download;
|
|
38
|
+
return { name: entry.name, version, repository, platform, asset: { ...asset },
|
|
39
|
+
url: `https://github.com/${repository}/releases/download/${tag}/${asset.file}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateNativeRelease(entry, release, platform) {
|
|
43
|
+
const expected = nativeRelease(entry, platform);
|
|
44
|
+
if (!release || JSON.stringify(release) !== JSON.stringify(expected)) fail('Release nativa diverge do pin revisado.');
|
|
45
|
+
return release;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function downloadNative(url, { fetcher = fetch, maxBytes = limit, timeoutMs = 60000 } = {}) {
|
|
49
|
+
let current = new URL(url);
|
|
50
|
+
if (current.hostname !== 'github.com') fail('Download nativo deve iniciar no GitHub oficial.');
|
|
51
|
+
for (let redirects = 0; redirects <= 5; redirects++) {
|
|
52
|
+
if (current.protocol !== 'https:' || current.username || current.password || !hosts.has(current.hostname)) fail('Host de download nativo nao permitido.');
|
|
53
|
+
const response = await fetcher(current.href, { redirect: 'manual', headers: { Accept: 'application/octet-stream' }, signal: AbortSignal.timeout(timeoutMs) });
|
|
54
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
55
|
+
const location = response.headers.get('location');
|
|
56
|
+
if (!location || redirects === 5) fail('Redirecionamento nativo invalido.');
|
|
57
|
+
current = new URL(location, current);
|
|
58
|
+
await response.body?.cancel();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok) fail(`Download nativo falhou: HTTP ${response.status}.`);
|
|
62
|
+
const declared = Number(response.headers.get('content-length'));
|
|
63
|
+
if (Number.isFinite(declared) && declared > maxBytes) fail('Download nativo excede o limite.');
|
|
64
|
+
const chunks = []; let size = 0;
|
|
65
|
+
for await (const chunk of response.body) {
|
|
66
|
+
size += chunk.byteLength;
|
|
67
|
+
if (size > maxBytes) fail('Download nativo excede o limite.');
|
|
68
|
+
chunks.push(Buffer.from(chunk));
|
|
69
|
+
}
|
|
70
|
+
if (!size) fail('Download nativo vazio.');
|
|
71
|
+
return Buffer.concat(chunks, size);
|
|
72
|
+
}
|
|
73
|
+
fail('Redirecionamentos nativos excedidos.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tarBytes(archive, binary) {
|
|
77
|
+
const data = gunzipSync(archive, { maxOutputLength: limit });
|
|
78
|
+
const names = new Set(); let selected, ended = false;
|
|
79
|
+
for (let offset = 0; offset + 512 <= data.length;) {
|
|
80
|
+
const header = data.subarray(offset, offset + 512);
|
|
81
|
+
if (header.every(value => value === 0)) {
|
|
82
|
+
if (data.length - offset < 1024 || !data.subarray(offset).every(value => value === 0)) fail('Final tar invalido.');
|
|
83
|
+
ended = true; break;
|
|
84
|
+
}
|
|
85
|
+
const text = (start, length) => header.subarray(start, start + length).toString('utf8').split('\0')[0];
|
|
86
|
+
const number = (start, length) => { const value = text(start, length).trim(); if (!/^[0-7]+$/.test(value)) fail('Cabecalho tar invalido.'); return parseInt(value, 8); };
|
|
87
|
+
const checksum = [...header].reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
|
|
88
|
+
if (number(148, 8) !== checksum) fail('Checksum do cabecalho tar invalido.');
|
|
89
|
+
const type = String.fromCharCode(header[156]), directory = type === '5';
|
|
90
|
+
if (!['0', '\0', '5'].includes(type)) fail('Links ou entradas especiais nao permitidos no tar.');
|
|
91
|
+
const name = safeName([text(345, 155), text(0, 100)].filter(Boolean).join('/'), directory);
|
|
92
|
+
const key = name.toLowerCase(); if (names.has(key)) fail('Entrada duplicada no tar.'); names.add(key);
|
|
93
|
+
const size = number(124, 12); offset += 512;
|
|
94
|
+
if (size > limit || offset + size > data.length || directory && size) fail('Tamanho tar invalido.');
|
|
95
|
+
if (!directory && name === binary) selected = Buffer.from(data.subarray(offset, offset + size));
|
|
96
|
+
offset += Math.ceil(size / 512) * 512;
|
|
97
|
+
}
|
|
98
|
+
if (!ended) fail('Arquivo tar incompleto.');
|
|
99
|
+
if (!selected?.length) fail('Executavel esperado ausente no tar.');
|
|
100
|
+
return selected;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function crc32(bytes) {
|
|
104
|
+
let crc = 0xffffffff;
|
|
105
|
+
for (const byte of bytes) { crc ^= byte; for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); }
|
|
106
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function zipBytes(data, binary) {
|
|
110
|
+
let end = -1;
|
|
111
|
+
for (let offset = data.length - 22; offset >= Math.max(0, data.length - 65557); offset--) {
|
|
112
|
+
if (data.readUInt32LE(offset) === 0x06054b50 && offset + 22 + data.readUInt16LE(offset + 20) === data.length) { end = offset; break; }
|
|
113
|
+
}
|
|
114
|
+
if (end < 0 || data.readUInt16LE(end + 4) || data.readUInt16LE(end + 6)) fail('Arquivo ZIP invalido ou multipartes.');
|
|
115
|
+
const count = data.readUInt16LE(end + 10), centralSize = data.readUInt32LE(end + 12), central = data.readUInt32LE(end + 16);
|
|
116
|
+
if (count === 0xffff || count !== data.readUInt16LE(end + 8) || central + centralSize !== end) fail('Diretorio ZIP invalido ou ZIP64.');
|
|
117
|
+
const names = new Set(); let selected, offset = central;
|
|
118
|
+
for (let index = 0; index < count; index++) {
|
|
119
|
+
if (offset + 46 > end || data.readUInt32LE(offset) !== 0x02014b50) fail('Entrada ZIP invalida.');
|
|
120
|
+
const flags = data.readUInt16LE(offset + 8), method = data.readUInt16LE(offset + 10), crc = data.readUInt32LE(offset + 16);
|
|
121
|
+
const compressed = data.readUInt32LE(offset + 20), size = data.readUInt32LE(offset + 24);
|
|
122
|
+
const nameLength = data.readUInt16LE(offset + 28), extra = data.readUInt16LE(offset + 30), comment = data.readUInt16LE(offset + 32);
|
|
123
|
+
const disk = data.readUInt16LE(offset + 34), attributes = data.readUInt32LE(offset + 38), local = data.readUInt32LE(offset + 42);
|
|
124
|
+
if (offset + 46 + nameLength + extra + comment > end || flags & 1 || disk || ![0, 8].includes(method) || size > limit || compressed > limit) fail('Formato ZIP nao permitido.');
|
|
125
|
+
const original = data.subarray(offset + 46, offset + 46 + nameLength).toString('utf8');
|
|
126
|
+
const directory = original.endsWith('/'), name = safeName(original, directory), mode = attributes >>> 16, type = mode & 0o170000;
|
|
127
|
+
if (![0, 0o100000, 0o040000].includes(type) || type === 0o040000 && !directory) fail('Link ou entrada especial no ZIP.');
|
|
128
|
+
const key = name.toLowerCase(); if (names.has(key)) fail('Entrada duplicada no ZIP.'); names.add(key);
|
|
129
|
+
if (local + 30 > central || data.readUInt32LE(local) !== 0x04034b50 || data.readUInt16LE(local + 6) !== flags || data.readUInt16LE(local + 8) !== method) fail('Cabecalho local ZIP divergente.');
|
|
130
|
+
const localNameLength = data.readUInt16LE(local + 26), localExtra = data.readUInt16LE(local + 28), start = local + 30 + localNameLength + localExtra;
|
|
131
|
+
if (start + compressed > central || data.subarray(local + 30, local + 30 + localNameLength).toString('utf8') !== original) fail('Caminho local ZIP divergente.');
|
|
132
|
+
if (!directory && name === binary) {
|
|
133
|
+
const packed = data.subarray(start, start + compressed);
|
|
134
|
+
selected = method === 0 ? Buffer.from(packed) : inflateRawSync(packed, { maxOutputLength: limit });
|
|
135
|
+
if (selected.length !== size || crc32(selected) !== crc) fail('Integridade do executavel ZIP invalida.');
|
|
136
|
+
}
|
|
137
|
+
offset += 46 + nameLength + extra + comment;
|
|
138
|
+
}
|
|
139
|
+
if (offset !== end || !selected?.length) fail('Executavel esperado ausente no ZIP.');
|
|
140
|
+
return selected;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function extractNative(archive, asset) {
|
|
144
|
+
if (!Buffer.isBuffer(archive) || !archive.length || archive.length > limit) fail('Tamanho de arquivo nativo invalido.');
|
|
145
|
+
safeName(asset.binary);
|
|
146
|
+
if (nativeHash(archive) !== asset.sha256) fail('SHA-256 do download nativo diverge do pin revisado.');
|
|
147
|
+
if (asset.format === 'raw') return archive;
|
|
148
|
+
if (asset.format === 'tar.gz') return tarBytes(archive, asset.binary);
|
|
149
|
+
if (asset.format === 'zip') return zipBytes(archive, asset.binary);
|
|
150
|
+
fail('Formato nativo nao suportado.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkedFile(prefix, relativePath) {
|
|
154
|
+
safeName(relativePath);
|
|
155
|
+
const base = resolve(prefix), file = resolve(base, relativePath), child = relative(base, file);
|
|
156
|
+
if (isAbsolute(child) || child === '..' || child.startsWith(`..${sep}`)) fail('Executavel fora da instalacao.');
|
|
157
|
+
if (!lstatSync(base).isDirectory() || lstatSync(base).isSymbolicLink()) fail('Raiz nativa deve ser um diretorio regular.');
|
|
158
|
+
let current = base;
|
|
159
|
+
for (const segment of child.split(sep)) {
|
|
160
|
+
current = join(current, segment);
|
|
161
|
+
try { if (lstatSync(current).isSymbolicLink()) fail('Link simbolico na instalacao nativa.'); }
|
|
162
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
163
|
+
}
|
|
164
|
+
return file;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function nativeExecutable(entry, platform) { return `bin/${entry.name}${platform.startsWith('win32-') ? '.exe' : ''}`; }
|
|
168
|
+
|
|
169
|
+
export async function installNative(prefix, entry, platform, { download = downloadNative } = {}) {
|
|
170
|
+
const release = nativeRelease(entry, platform), archive = await download(release.url), bytes = extractNative(archive, release.asset);
|
|
171
|
+
const executable = nativeExecutable(entry, platform), target = checkedFile(prefix, executable);
|
|
172
|
+
mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, bytes, { flag: 'wx', mode: 0o755 }); chmodSync(target, 0o755);
|
|
173
|
+
const receipt = { name: entry.name, version: release.version, platform, executable, archiveSha256: release.asset.sha256, binarySha256: nativeHash(bytes) };
|
|
174
|
+
writeFileSync(checkedFile(prefix, 'native.json'), JSON.stringify(receipt, null, 2) + '\n', { flag: 'wx' });
|
|
175
|
+
return receipt;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function checkNative(prefix, entry, platform) {
|
|
179
|
+
const file = checkedFile(prefix, 'native.json');
|
|
180
|
+
if (!lstatSync(file).isFile()) fail('Recibo nativo invalido.');
|
|
181
|
+
const receipt = JSON.parse(readFileSync(file, 'utf8'));
|
|
182
|
+
const release = nativeRelease(entry, platform);
|
|
183
|
+
if (receipt.name !== entry.name || receipt.version !== release.version || receipt.platform !== platform
|
|
184
|
+
|| receipt.executable !== nativeExecutable(entry, platform) || receipt.archiveSha256 !== release.asset.sha256
|
|
185
|
+
|| !/^[a-f0-9]{64}$/.test(receipt.binarySha256 || '')) fail('Recibo nativo diverge do catalogo revisado.');
|
|
186
|
+
const executable = checkedFile(prefix, receipt.executable);
|
|
187
|
+
if (!lstatSync(executable).isFile() || nativeHash(readFileSync(executable)) !== receipt.binarySha256) fail('Executavel nativo alterado.');
|
|
188
|
+
return { ...receipt, file: executable };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function assertNativeVersion(output, version) {
|
|
192
|
+
if (!new RegExp(`(?:^|[^\\w.])v?${version.replaceAll('.', '\\.')}(?=$|[^\\w.+-])`).test(output)) fail('Probe nativo informou outra versao.');
|
|
193
|
+
}
|
|
@@ -54,14 +54,20 @@ Locks ativos, de outro host ou sem metadados verificáveis são preservados.
|
|
|
54
54
|
Não aceita grupos. Um lock ausente não cria arquivos.
|
|
55
55
|
|
|
56
56
|
O requisito comum é Node.js 24+ com npm. O gerenciador usa o Node atual.
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
Pacotes npm usam o registro oficial com scripts de instalação desativados.
|
|
58
|
+
Scanners nativos usam assets oficiais do GitHub com versão e SHA-256 revisados.
|
|
59
|
+
`update` segue `latest` no npm e o pin do catálogo nos scanners nativos.
|
|
60
|
+
Os dois caminhos conferem integridade antes de executar e preservam a versão
|
|
61
|
+
anterior se a atualização falhar.
|
|
59
62
|
Permissões de binários POSIX são aplicadas somente aos caminhos declarados
|
|
60
63
|
em `executables` no `TOOL.json`, antes do probe.
|
|
61
64
|
Atualizações usam staging e preservam a versão anterior em caso de falha.
|
|
62
65
|
Atalhos locais recusam diretórios e arquivos simbólicos antes de escrever.
|
|
63
66
|
O preparo valida os CLIs. Navegador, permissões desktop e FFmpeg dependem da
|
|
64
67
|
tarefa; ele não inicia aplicações nem concede permissões automaticamente.
|
|
68
|
+
No grupo `security`, a base OSV também é separada do CLI. Use o comando
|
|
69
|
+
[preparar-seguranca](../../commands/preparar-seguranca/COMMAND.md) para orientar
|
|
70
|
+
instalação, atualização explícita dos dados e análise offline.
|
|
65
71
|
|
|
66
72
|
Testes: `node --test .pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs`.
|
|
67
73
|
Após instalar `skills`, `just-bash`, `opensrc`, `webreel` e `portless`, execute
|