@pathbit/agentdevkit 0.0.10 → 0.0.12
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/documentacao/RULE.md +2 -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/README.md +4 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
- package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
- package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -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/scripts/sincronizar-agentes/README.md +9 -7
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
- package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
- 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 +17 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +8 -3
- package/docs/README.md +6 -3
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +2 -1
- package/docs/guides/installation.md +25 -8
- package/docs/guides/resources.md +48 -1
- package/docs/guides/security-references.md +97 -0
- package/docs/guides/security.md +32 -0
- package/docs/guides/troubleshooting.md +8 -5
- package/docs/maintainers/maintenance.md +12 -0
- package/docs/maintainers/packages.md +2 -2
- package/docs/product/design.md +10 -3
- package/docs/product/prd.md +5 -1
- package/docs/product/roadmap.md +4 -0
- package/docs/specs/README.md +4 -0
- package/docs/specs/additive-integration/README.md +9 -0
- package/docs/specs/additive-integration/SPEC.md +53 -0
- package/docs/specs/security-ecosystem/README.md +9 -0
- package/docs/specs/security-ecosystem/SPEC.md +53 -0
- package/package.json +1 -1
- package/src/install.mjs +139 -63
|
@@ -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
|
+
}
|
|
@@ -7,3 +7,7 @@ description: Utilitários mínimos compartilhados das automações Node.js.
|
|
|
7
7
|
`runtime.mjs` centraliza o requisito Node.js 24+, a raiz do repositório e a
|
|
8
8
|
identificação do arquivo executado. Não instala runtimes nem configura a máquina.
|
|
9
9
|
Também procura executáveis opcionais no PATH sem iniciar um shell.
|
|
10
|
+
|
|
11
|
+
`managed-content.mjs` limita a propriedade do kit aos seus blocos Markdown;
|
|
12
|
+
`managed-toml.mjs` aplica o mesmo contrato ao `prompt` de comandos Gemini.
|
|
13
|
+
Texto externo, nomes existentes e metadados humanos permanecem preservados.
|
|
@@ -125,6 +125,34 @@ function artifactPolicy(templateFile) {
|
|
|
125
125
|
return patterns;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function privateClientPolicy(templateFile, roots) {
|
|
129
|
+
const adjacent = join(dirname(templateFile), 'agents.ignore');
|
|
130
|
+
const source = checkedLocalPath(localStat(adjacent) ? adjacent : fileURLToPath(new URL('../../templates/gitignore/agents.ignore', import.meta.url)));
|
|
131
|
+
const section = readFileSync(source, 'utf8').split(/^## Estado local, caches e credenciais conhecidas\r?$/m)[1]?.split(/^## /m)[0];
|
|
132
|
+
const patterns = section?.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#'));
|
|
133
|
+
if (!patterns?.length || patterns.some(pattern => !/^(?:!?\*\*\/|\/)/.test(pattern) || /[\\\x00-\x1f]/.test(pattern) || pattern.split('/').includes('..'))) throw new Error('Secao de estado local invalida em agents.ignore.');
|
|
134
|
+
const rules = [], probes = [];
|
|
135
|
+
for (const root of roots) for (const pattern of patterns) {
|
|
136
|
+
const negative = pattern.startsWith('!'), rule = pattern.replace(/^!/, '').replace(/^\//, '');
|
|
137
|
+
rules.push(`${negative ? '!' : ''}/${escapePattern(root)}/${rule}`);
|
|
138
|
+
if (negative) continue;
|
|
139
|
+
const stem = rule.replace(/^\*\*\//, '').replace(/\[([^\]]+)\]/g, (_match, letters) => letters[0]).replace(/\*/g, 'agentdevkit-private-probe').replace(/\?/g, 'x');
|
|
140
|
+
probes.push(`${root}/${stem}${stem.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
|
|
141
|
+
if (rule.startsWith('**/')) probes.push(`${root}/.agentdevkit-ignore-probes/${stem}${stem.endsWith('/') ? '.agentdevkit-ignore-probe' : ''}`);
|
|
142
|
+
}
|
|
143
|
+
return { rules, probes };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function safeIgnoreProbe(root, path) {
|
|
147
|
+
let parent = root;
|
|
148
|
+
for (const part of path.split('/').slice(0, -1)) {
|
|
149
|
+
parent = join(parent, part);
|
|
150
|
+
const item = localStat(parent);
|
|
151
|
+
if (item && !item.isDirectory()) return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
function artifactProbes(root, patterns) {
|
|
129
157
|
const probes = new Set();
|
|
130
158
|
for (const pattern of patterns) {
|
|
@@ -233,10 +261,14 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
233
261
|
const artifacts = artifactPolicy(templateFile);
|
|
234
262
|
if (template.split('{{OUTPUTS}}').length !== 2) throw new Error('local.ignore exige um unico {{OUTPUTS}}.');
|
|
235
263
|
if (template.split('{{ROOTS}}').length > 2) throw new Error('local.ignore permite um unico {{ROOTS}}.');
|
|
264
|
+
// A client directory can also contain human instructions and settings. Only
|
|
265
|
+
// whole files owned by the kit belong in the local output exclusion list.
|
|
266
|
+
const roots = [];
|
|
236
267
|
const localPolicy = join(templateRoot || root, '.pbai4devs/templates/clients/ADAPTERS.json');
|
|
237
268
|
const policy = JSON.parse(readFileSync(checkedLocalPath(localStat(localPolicy) ? localPolicy : fileURLToPath(new URL('../../templates/clients/ADAPTERS.json', import.meta.url))), 'utf8'));
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
269
|
+
const clientRoots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))];
|
|
270
|
+
if (clientRoots.some(path => typeof path !== 'string' || !/^\.[a-z0-9][a-z0-9-]*$/.test(path) || ['.git', '.pbai4devs', '.agent-tests'].includes(path))) throw new Error('Raiz de cliente invalida para exclusao.');
|
|
271
|
+
const privateClients = privateClientPolicy(templateFile, clientRoots);
|
|
240
272
|
const paths = [...new Set([...outputs.map(checkedOutput), ...template.split(/\r?\n/).filter(line => line.startsWith('/')).map(line => checkedOutput(line.slice(1)))])].sort();
|
|
241
273
|
const info = gitCommand(resolved.parent, ['rev-parse', '--show-toplevel', '--git-path', 'info/exclude']);
|
|
242
274
|
let location = null;
|
|
@@ -256,13 +288,18 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
256
288
|
const id = createHash('sha256').update(root).digest('hex').slice(0, 16);
|
|
257
289
|
const first = `# BEGIN AGENTDEVKIT LOCAL ${id}`, last = `# END AGENTDEVKIT LOCAL ${id}`;
|
|
258
290
|
const fixed = template.replace('{{ROOTS}}', roots.map(path => `/${path}/`).join('\n')).replace('{{OUTPUTS}}', paths.filter(path => !roots.some(root => path === root || path.startsWith(root + '/'))).map(path => `/${escapePattern(path)}`).join('\n'))
|
|
291
|
+
+ '\n# Estado privado dos clientes\n' + privateClients.rules.join('\n') + '\n'
|
|
259
292
|
+ '\n# Resultados e arquivos temporarios\n' + artifacts.map(pattern => `/${pattern}`).join('\n') + '\n';
|
|
260
|
-
const body = fixed.split(/\r?\n/).map(line =>
|
|
293
|
+
const body = fixed.split(/\r?\n/).map(line => {
|
|
294
|
+
if (!location?.prefix) return line;
|
|
295
|
+
if (line.startsWith('!/')) return `!/${escapePattern(location.prefix)}${line.slice(1)}`;
|
|
296
|
+
return line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line;
|
|
297
|
+
}).join('\n');
|
|
261
298
|
const target = location?.exclude ?? checkedLocalPath(join(root, '.gitignore'));
|
|
262
299
|
const before = localRead(target), after = localBlock(before, first, last, body);
|
|
263
300
|
const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
|
|
264
301
|
const probes = [...paths, ...rootProbes(root, roots)];
|
|
265
|
-
const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
|
|
302
|
+
const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...privateClients.probes.filter(path => safeIgnoreProbe(root, path)), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
|
|
266
303
|
const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
|
|
267
304
|
if (location && !before?.includes(first) && !localRead(join(root, '.gitignore'))?.includes(first)) {
|
|
268
305
|
const ignored = ignoreMatches(location, [...probes, ...artifactPaths]);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
export const managedBegin = '<!-- BEGIN AGENTDEVKIT -->';
|
|
5
|
+
export const managedEnd = '<!-- END AGENTDEVKIT -->';
|
|
6
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
7
|
+
const fail = message => { throw Object.assign(new Error(message), { code: 'E_MANAGED_BLOCK' }); };
|
|
8
|
+
|
|
9
|
+
export function canonicalEntrypoint(path) {
|
|
10
|
+
return ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].find(name => name.toLowerCase() === path.toLowerCase()) || null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveEntrypoint(root, canonical) {
|
|
14
|
+
if (!canonicalEntrypoint(canonical)) fail(`Entrada desconhecida: ${canonical}`);
|
|
15
|
+
let names;
|
|
16
|
+
try { names = readdirSync(root); }
|
|
17
|
+
catch (error) { if (error.code === 'ENOENT') return canonical; throw error; }
|
|
18
|
+
const matches = names.filter(name => name.toLowerCase() === canonical.toLowerCase());
|
|
19
|
+
if (matches.length > 1) fail(`Entradas ambiguas: ${matches.sort().join(', ')}`);
|
|
20
|
+
return matches[0] || canonical;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function entrypointReference(root, path) {
|
|
24
|
+
const canonical = canonicalEntrypoint(path);
|
|
25
|
+
if (canonical === 'AGENTS.md') return 'Leia e siga [.pbai4devs/INSTRUCTIONS.md](.pbai4devs/INSTRUCTIONS.md).';
|
|
26
|
+
if (canonical === 'CLAUDE.md') return `@${resolveEntrypoint(root, 'AGENTS.md')}`;
|
|
27
|
+
if (canonical === 'GEMINI.md') return `@./${resolveEntrypoint(root, 'AGENTS.md')}`;
|
|
28
|
+
fail(`Entrada desconhecida: ${path}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderEntrypointImports(root, text) {
|
|
32
|
+
const name = resolveEntrypoint(root, 'AGENTS.md');
|
|
33
|
+
return text.replace(/^(@(?:\.\/)?)AGENTS\.md(?=\r?$)/gim, (_match, prefix) => `${prefix}${name}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function managedText(value) {
|
|
37
|
+
const text = Buffer.isBuffer(value) ? new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(value) : String(value ?? '');
|
|
38
|
+
if (text.includes('\0')) fail('Conteudo binario nao pode receber instrucoes Markdown.');
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function locate(value) {
|
|
43
|
+
const text = managedText(value);
|
|
44
|
+
let fence = null, start = null, finish = null, offset = 0;
|
|
45
|
+
for (const match of text.matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)) {
|
|
46
|
+
if (!match[0]) continue;
|
|
47
|
+
const line = match[1];
|
|
48
|
+
const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
49
|
+
if (fence) {
|
|
50
|
+
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = null;
|
|
51
|
+
} else if (marker) fence = marker[1];
|
|
52
|
+
else if (line === managedBegin) {
|
|
53
|
+
if (start !== null || finish !== null) fail('Marcadores AgentDevKit duplicados.');
|
|
54
|
+
start = offset;
|
|
55
|
+
} else if (line === managedEnd) {
|
|
56
|
+
if (start === null || finish !== null) fail('Marcadores AgentDevKit incompletos.');
|
|
57
|
+
finish = offset + line.length;
|
|
58
|
+
}
|
|
59
|
+
offset += match[0].length;
|
|
60
|
+
}
|
|
61
|
+
if ((start === null) !== (finish === null)) fail('Marcadores AgentDevKit incompletos.');
|
|
62
|
+
return { text, start, finish, fence, block: start === null ? null : text.slice(start, finish) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function extractManagedBlock(value) { return locate(value).block; }
|
|
66
|
+
export function managedBlockDigest(value) { const block = extractManagedBlock(value); return block === null ? null : digest(block); }
|
|
67
|
+
|
|
68
|
+
export function mergeManagedContent(value, body, { expectedHash } = {}) {
|
|
69
|
+
const current = locate(value), eol = current.text.match(/\r\n|\n|\r/)?.[0] || '\n';
|
|
70
|
+
const content = managedText(body).trim().replace(/\r\n|\r|\n/g, eol);
|
|
71
|
+
if (!content || content.includes(managedBegin) || content.includes(managedEnd)) fail('Referencia AgentDevKit invalida.');
|
|
72
|
+
const block = `${managedBegin}${eol}${content}${eol}${managedEnd}`;
|
|
73
|
+
if (current.block !== null && (expectedHash ? digest(current.block) !== expectedHash : current.block !== block)) fail('Bloco AgentDevKit alterado; conteudo preservado.');
|
|
74
|
+
if (current.fence && current.block === null) fail('Bloco de codigo Markdown nao fechado; conteudo preservado.');
|
|
75
|
+
const separator = !current.text || current.text.endsWith(eol + eol) ? '' : /[\r\n]$/.test(current.text) ? eol : eol + eol;
|
|
76
|
+
const text = current.block === null ? `${current.text}${separator}${block}${eol}`
|
|
77
|
+
: current.text.slice(0, current.start) + block + current.text.slice(current.finish);
|
|
78
|
+
return { text, block, sha256: digest(block), changed: text !== current.text };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function stripManagedContent(value, expectedHash) {
|
|
82
|
+
const current = locate(value);
|
|
83
|
+
if (current.block === null) return current.text;
|
|
84
|
+
if (!expectedHash || digest(current.block) !== expectedHash) fail('Bloco AgentDevKit alterado; conteudo preservado.');
|
|
85
|
+
return current.text.slice(0, current.start) + current.text.slice(current.finish);
|
|
86
|
+
}
|