@principles/core 1.229.0 → 1.231.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3441,4 +3441,313 @@ describe('PRI-450 / PRI-462: core I/O seam registry guard', () => {
3441
3441
  expect(importsFsOrPath(`import type { Database } from 'better-sqlite3';`)).toBe(false);
3442
3442
  });
3443
3443
  });
3444
+ // ===========================================================================
3445
+ // SEC-BASE-1: Supply Chain Provenance Guards
3446
+ // Guards the npm publish / CI supply chain controls declared in
3447
+ // docs/architecture/SECURITY_BASELINE.md §3 (supply chain layer).
3448
+ // ===========================================================================
3449
+ describe('SEC-BASE-1: supply chain provenance guards', () => {
3450
+ const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
3451
+ function readWorkflow(name) {
3452
+ const p = pathSync.join(REPO_ROOT, '.github', 'workflows', name);
3453
+ try {
3454
+ return fsSync.readFileSync(p, 'utf8');
3455
+ }
3456
+ catch {
3457
+ return null;
3458
+ }
3459
+ }
3460
+ it('publish-npm.yml uses --provenance', () => {
3461
+ const content = readWorkflow('publish-npm.yml');
3462
+ expect(content, 'publish-npm.yml should exist').not.toBeNull();
3463
+ expect(content).toMatch(/npm\s+publish\s+--provenance/);
3464
+ });
3465
+ it('publish-npm.yml uses npm ci --ignore-scripts', () => {
3466
+ const content = readWorkflow('publish-npm.yml');
3467
+ expect(content).not.toBeNull();
3468
+ expect(content).toMatch(/npm\s+ci\s+--ignore-scripts/);
3469
+ });
3470
+ it('SECURITY.md exists at .github/SECURITY.md', () => {
3471
+ const p = pathSync.join(REPO_ROOT, '.github', 'SECURITY.md');
3472
+ expect(fsSync.existsSync(p), `${p} should exist`).toBe(true);
3473
+ });
3474
+ it('CodeQL workflow targets javascript-typescript', () => {
3475
+ const content = readWorkflow('codeql.yml');
3476
+ expect(content, 'codeql.yml should exist').not.toBeNull();
3477
+ expect(content).toMatch(/javascript-typescript/);
3478
+ });
3479
+ it('dependabot.yml covers npm and github-actions', () => {
3480
+ const p = pathSync.join(REPO_ROOT, '.github', 'dependabot.yml');
3481
+ const content = fsSync.readFileSync(p, 'utf8');
3482
+ expect(content).toMatch(/package-ecosystem:\s*['"]?npm['"]?/m);
3483
+ expect(content).toMatch(/package-ecosystem:\s*['"]?github-actions['"]?/m);
3484
+ });
3485
+ });
3486
+ // ===========================================================================
3487
+ // SEC-BASE-2: Sandbox Escape Regression Guards
3488
+ // Guards the vm sandbox layer declared in SECURITY_BASELINE.md §4.
3489
+ // Escape payload regression lives in
3490
+ // packages/openclaw-plugin/tests/core/sandbox-escape-regression.test.ts
3491
+ // ===========================================================================
3492
+ describe('SEC-BASE-2: sandbox escape regression guards', () => {
3493
+ const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
3494
+ it('rule-code-validator forbids import.meta', async () => {
3495
+ const { checkForbiddenPatterns } = await import('../internalization/rule-code-validator.js');
3496
+ const labels = checkForbiddenPatterns('const u = import.meta.url;');
3497
+ expect(labels).toContain('import.meta');
3498
+ });
3499
+ it('rule-code-validator forbids WeakRef / FinalizationRegistry', async () => {
3500
+ const { checkForbiddenPatterns } = await import('../internalization/rule-code-validator.js');
3501
+ expect(checkForbiddenPatterns('new WeakRef({});')).toContain('WeakRef');
3502
+ expect(checkForbiddenPatterns('new FinalizationRegistry(() => {});')).toContain('FinalizationRegistry');
3503
+ });
3504
+ it('rule-code-validator forbids SharedArrayBuffer / Atomics', async () => {
3505
+ const { checkForbiddenPatterns } = await import('../internalization/rule-code-validator.js');
3506
+ expect(checkForbiddenPatterns('new SharedArrayBuffer(8);')).toContain('SharedArrayBuffer');
3507
+ expect(checkForbiddenPatterns('Atomics.load(new Int32Array(1), 0);')).toContain('Atomics');
3508
+ });
3509
+ it('rule-implementation-runtime.ts uses spawnSync with bounded resources', () => {
3510
+ const sourcePath = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src', 'core', 'rule-implementation-runtime.ts');
3511
+ const source = fsSync.readFileSync(sourcePath, 'utf8');
3512
+ expect(source).toContain('spawnSync');
3513
+ expect(source).toContain('windowsHide: true');
3514
+ expect(source).toContain('maxBuffer');
3515
+ expect(source).toContain('--max-old-space-size');
3516
+ expect(source).toMatch(/timeout:\s*\w+/);
3517
+ });
3518
+ });
3519
+ // ===========================================================================
3520
+ // SEC-BASE-3: PII Redaction Guards
3521
+ // Guards that the implemented PII sanitization patterns exist in
3522
+ // openclaw-plugin (trajectory.ts redactText) and that message-sanitize hook
3523
+ // applies sanitization to message content.
3524
+ //
3525
+ // IMPLEMENTED (MVP): <EMAIL>, <TOKEN> (sk/rk/pk prefix), <PATH>, <WINDOWS_PATH>
3526
+ // POST-MVP GAPS (documented in SECURITY_BASELINE.md): <PHONE>, <CARD>, <IP>
3527
+ // ===========================================================================
3528
+ describe('SEC-BASE-3: PII redaction guards', () => {
3529
+ const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
3530
+ it('openclaw-plugin contains email redaction pattern with <EMAIL> replacement', () => {
3531
+ const pluginDir = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src');
3532
+ const files = fsSync.readdirSync(pluginDir, { recursive: true, encoding: 'utf8' })
3533
+ .filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
3534
+ let foundEmail = false;
3535
+ for (const f of files) {
3536
+ const content = fsSync.readFileSync(pathSync.join(pluginDir, f), 'utf8');
3537
+ // Look for: <EMAIL> replacement token AND a regex containing @ (email pattern)
3538
+ if (/<EMAIL>/.test(content) && /\.\s*replace\s*\(\s*\/.*@.*\//.test(content)) {
3539
+ foundEmail = true;
3540
+ break;
3541
+ }
3542
+ }
3543
+ expect(foundEmail, 'openclaw-plugin should contain email redaction pattern with <EMAIL> replacement').toBe(true);
3544
+ });
3545
+ it('openclaw-plugin contains API token redaction pattern (<TOKEN> with sk/rk/pk prefix)', () => {
3546
+ const pluginDir = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src');
3547
+ const files = fsSync.readdirSync(pluginDir, { recursive: true, encoding: 'utf8' })
3548
+ .filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
3549
+ let foundToken = false;
3550
+ for (const f of files) {
3551
+ const content = fsSync.readFileSync(pathSync.join(pluginDir, f), 'utf8');
3552
+ // Look for: <TOKEN> replacement token AND a regex containing (sk|rk|pk)
3553
+ if (/<TOKEN>/.test(content) && /\(sk\|rk\|pk\)/.test(content)) {
3554
+ foundToken = true;
3555
+ break;
3556
+ }
3557
+ }
3558
+ expect(foundToken, 'openclaw-plugin should contain API token redaction pattern with <TOKEN> replacement').toBe(true);
3559
+ });
3560
+ it('openclaw-plugin message-sanitize hook applies sanitization', () => {
3561
+ const sourcePath = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src', 'hooks', 'message-sanitize.ts');
3562
+ const source = fsSync.readFileSync(sourcePath, 'utf8');
3563
+ // Delegate pattern: import from core + apply to message content
3564
+ expect(source).toMatch(/coreSanitize|sanitizeValue|sanitiz/i);
3565
+ });
3566
+ });
3567
+ // ===========================================================================
3568
+ // SEC-BASE-4: Log Secret Redaction & SQLite SQL Injection Guards
3569
+ // Guards that:
3570
+ // (a) core does not emit raw secret-named fields in logger output
3571
+ // (b) core does not string-concatenate SQL (uses parameterized queries)
3572
+ // ===========================================================================
3573
+ describe('SEC-BASE-4: log secret redaction & SQL injection guards', () => {
3574
+ const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
3575
+ it('core source does not log raw api_key / token / secret / password fields', () => {
3576
+ const coreDir = pathSync.join(REPO_ROOT, 'packages', 'principles-core', 'src');
3577
+ const files = fsSync.readdirSync(coreDir, { recursive: true, encoding: 'utf8' })
3578
+ .filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
3579
+ const suspicious = [];
3580
+ for (const f of files) {
3581
+ const content = fsSync.readFileSync(pathSync.join(coreDir, f), 'utf8');
3582
+ const lines = content.split('\n');
3583
+ lines.forEach((line, idx) => {
3584
+ if (/log(ger)?\./i.test(line) || /console\./.test(line)) {
3585
+ if (/\b(api[_-]?key|token|secret|password|credential)\b/i.test(line) && !/<REDACTED>/.test(line) && !/sanitiz/i.test(line)) {
3586
+ suspicious.push(`${f}:${idx + 1}: ${line.trim()}`);
3587
+ }
3588
+ }
3589
+ });
3590
+ }
3591
+ expect(suspicious, `Suspicious unredacted logger lines in core:\n${suspicious.join('\n')}`).toEqual([]);
3592
+ });
3593
+ it('core source does not string-concatenate SQL statements', () => {
3594
+ const coreDir = pathSync.join(REPO_ROOT, 'packages', 'principles-core', 'src');
3595
+ const files = fsSync.readdirSync(coreDir, { recursive: true, encoding: 'utf8' })
3596
+ .filter((f) => typeof f === 'string'
3597
+ && f.endsWith('.ts')
3598
+ && !f.endsWith('.test.ts')
3599
+ && !f.includes('__tests__'));
3600
+ const suspicious = [];
3601
+ // Known-safe pre-built clause variables (constructed from controlled
3602
+ // conditions with `?` placeholders for values — not user input).
3603
+ // `placeholders` = e.g. `ids.map(() => '?').join(',')` → generates `?,?,?`
3604
+ // (a string of `?` placeholder characters, NOT user values).
3605
+ const SAFE_CLAUSE_VARS = new Set([
3606
+ 'whereClause', 'orderByClause', 'limitClause', 'setClause',
3607
+ 'conflictClause', 'returningClause', 'onConflictClause',
3608
+ 'placeholders',
3609
+ ]);
3610
+ // DML statements that MUST use parameterized values (`?` placeholders).
3611
+ // DDL (ALTER TABLE / CREATE TABLE / CREATE INDEX / DROP) is excluded
3612
+ // because SQLite does not accept `?` placeholders for identifiers (column
3613
+ // names, table names) — DDL string-concat with controlled constants is
3614
+ // the only valid pattern and is verified separately.
3615
+ const DML_RE = /\b(SELECT|INSERT\s+INTO|UPDATE\s+\w|DELETE\s+FROM)\b/i;
3616
+ for (const f of files) {
3617
+ const content = fsSync.readFileSync(pathSync.join(coreDir, f), 'utf8');
3618
+ // Find line number offset for reporting.
3619
+ const lines = content.split('\n');
3620
+ // Match .prepare(`...`) and .exec(`...`) template-literal SQL arguments.
3621
+ // Uses [\s\S]*? (non-greedy, dotAll-equivalent) to capture multi-line
3622
+ // template literals. Only the FIRST argument (the SQL string) is inspected.
3623
+ const templateSqlRe = /\.(prepare|exec)\s*\(\s*`([\s\S]*?)`/g;
3624
+ let m;
3625
+ while ((m = templateSqlRe.exec(content)) !== null) {
3626
+ // rc-2-no-as-bypass: RegExpExecArray indices are `string | undefined`
3627
+ // under noUncheckedIndexedAccess. Use array destructuring (ESLint
3628
+ // prefer-destructuring) + explicit undefined check (fail-loud).
3629
+ const [, , sqlBody] = m;
3630
+ if (sqlBody === undefined)
3631
+ continue;
3632
+ if (!DML_RE.test(sqlBody))
3633
+ continue; // DDL-only is allowed
3634
+ // Find line number of the match start.
3635
+ const matchOffset = m.index;
3636
+ const lineNum = content.slice(0, matchOffset).split('\n').length;
3637
+ // Check for `${X}` interpolations inside the SQL template body.
3638
+ const interpRe = /\$\{([^}]+)\}/g;
3639
+ let im;
3640
+ while ((im = interpRe.exec(sqlBody)) !== null) {
3641
+ const [, expr] = im;
3642
+ if (expr === undefined)
3643
+ continue;
3644
+ const exprTrimmed = expr.trim();
3645
+ // Safe: ends with .join(...) — column-list / placeholder-list pattern.
3646
+ if (/\.join\s*\(/.test(exprTrimmed))
3647
+ continue;
3648
+ // Safe: known pre-built clause variable.
3649
+ const rootVar = exprTrimmed.split('.')[0]?.split('[')[0]?.split('(')[0];
3650
+ if (rootVar !== undefined && SAFE_CLAUSE_VARS.has(rootVar.trim()))
3651
+ continue;
3652
+ // Safe: numeric / length / index expressions (not user data).
3653
+ if (/^\d+$/.test(exprTrimmed) || /\.length$/.test(exprTrimmed))
3654
+ continue;
3655
+ suspicious.push(`${f}:${lineNum}: interpolation \${${exprTrimmed}} in DML template`);
3656
+ }
3657
+ }
3658
+ // Also check for string-concatenation pattern: .prepare('...' + var + '...')
3659
+ // This is a single-line check (string concat SQL is rarely multi-line).
3660
+ const concatSqlRe = /\.(prepare|exec)\s*\(\s*['"][^'"]*['"]\s*\+/g;
3661
+ let cm;
3662
+ while ((cm = concatSqlRe.exec(content)) !== null) {
3663
+ const lineNum = content.slice(0, cm.index).split('\n').length;
3664
+ const line = lines[lineNum - 1] || '';
3665
+ // Only flag if the concatenated string contains DML keywords.
3666
+ if (DML_RE.test(line)) {
3667
+ suspicious.push(`${f}:${lineNum}: string-concat in DML: ${line.trim()}`);
3668
+ }
3669
+ }
3670
+ }
3671
+ expect(suspicious, `Suspicious string-concatenated or interpolated DML in core:\n${suspicious.join('\n')}`).toEqual([]);
3672
+ });
3673
+ });
3674
+ // ===========================================================================
3675
+ // SEC-BASE-5: Network & Config Isolation Guards
3676
+ // Guards:
3677
+ // (a) core does not import node:http / node:https / undici / fetch (NET-1)
3678
+ // (b) pd-console server config does not default to 0.0.0.0 (NET-5)
3679
+ // (c) code_tool_hook config defaults to require_approval (CFG-4)
3680
+ // ===========================================================================
3681
+ describe('SEC-BASE-5: network & config isolation guards', () => {
3682
+ const REPO_ROOT = pathSync.resolve(__dirname, '../../../../..');
3683
+ it('core does not import node:http / node:https / undici / fetch for business', () => {
3684
+ const coreDir = pathSync.join(REPO_ROOT, 'packages', 'principles-core', 'src');
3685
+ const files = fsSync.readdirSync(coreDir, { recursive: true, encoding: 'utf8' })
3686
+ .filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
3687
+ const violations = [];
3688
+ for (const f of files) {
3689
+ const content = fsSync.readFileSync(pathSync.join(coreDir, f), 'utf8');
3690
+ const lines = content.split('\n');
3691
+ lines.forEach((line, idx) => {
3692
+ if (/^import\s+type/.test(line.trim()))
3693
+ return;
3694
+ if (/from\s+['"]node:http['"]/.test(line) || /from\s+['"]node:https['"]/.test(line)
3695
+ || /from\s+['"]undici['"]/.test(line) || /from\s+['"]node-fetch['"]/.test(line)) {
3696
+ violations.push(`${f}:${idx + 1}: ${line.trim()}`);
3697
+ }
3698
+ if (!/^\s*(\/\/|\/\*|\*)/.test(line) && /\bfetch\s*\(/.test(line) && !/import/.test(line)) {
3699
+ violations.push(`${f}:${idx + 1}: ${line.trim()}`);
3700
+ }
3701
+ });
3702
+ }
3703
+ expect(violations, `core should not import network modules (NET-1):\n${violations.join('\n')}`).toEqual([]);
3704
+ });
3705
+ it('pd-console server config does not default to 0.0.0.0', () => {
3706
+ const consoleDir = pathSync.join(REPO_ROOT, 'packages', 'pd-console', 'src');
3707
+ if (!fsSync.existsSync(consoleDir)) {
3708
+ // pd-console may not exist in this worktree — skip with explicit note
3709
+ console.warn('SEC-BASE-5: pd-console src dir not found, skipping bind_host check');
3710
+ return;
3711
+ }
3712
+ const files = fsSync.readdirSync(consoleDir, { recursive: true, encoding: 'utf8' })
3713
+ .filter((f) => typeof f === 'string' && f.endsWith('.ts'));
3714
+ const violations = [];
3715
+ for (const f of files) {
3716
+ const content = fsSync.readFileSync(pathSync.join(consoleDir, f), 'utf8');
3717
+ const lines = content.split('\n');
3718
+ lines.forEach((line, idx) => {
3719
+ // rc-9-no-silent-fallback: previously used `!/comment/.test(line)` which
3720
+ // matched any line containing the substring "comment" (e.g. a variable
3721
+ // named `commentField`), silently skipping real 0.0.0.0 defaults.
3722
+ // Only skip genuine comment lines.
3723
+ const trimmed = line.trim();
3724
+ const isCommentLine = trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*');
3725
+ if (/host\s*[:=]\s*['"]0\.0\.0\.0['"]/.test(line) && !isCommentLine) {
3726
+ violations.push(`${f}:${idx + 1}: ${line.trim()}`);
3727
+ }
3728
+ });
3729
+ }
3730
+ expect(violations, `pd-console should not default to 0.0.0.0 (NET-5):\n${violations.join('\n')}`).toEqual([]);
3731
+ });
3732
+ it('code_tool_hook config defaults to require_approval', () => {
3733
+ // rc-9-no-silent-fallback: this was previously `expect(true).toBe(true)`
3734
+ // (a tautology that always passed, giving false confidence). Now it fails
3735
+ // loud if the require_approval decision is removed from the codebase.
3736
+ // The `requireApproval` decision is enforced in gate.ts and is the
3737
+ // MVP-Core safety boundary for code_tool_hook.
3738
+ const pluginDir = pathSync.join(REPO_ROOT, 'packages', 'openclaw-plugin', 'src');
3739
+ const files = fsSync.readdirSync(pluginDir, { recursive: true, encoding: 'utf8' })
3740
+ .filter((f) => typeof f === 'string' && f.endsWith('.ts') && !f.includes('__tests__'));
3741
+ let foundRequireApproval = false;
3742
+ for (const f of files) {
3743
+ const content = fsSync.readFileSync(pathSync.join(pluginDir, f), 'utf8');
3744
+ if (/code[_-]?tool[_-]?hook/i.test(content) && /require[_-]?approval/i.test(content)) {
3745
+ foundRequireApproval = true;
3746
+ break;
3747
+ }
3748
+ }
3749
+ expect(foundRequireApproval, 'code_tool_hook should reference require_approval decision in openclaw-plugin src ' +
3750
+ '(expected in hooks/gate.ts). If removed, the code_tool_hook safety boundary is broken.').toBe(true);
3751
+ });
3752
+ });
3444
3753
  //# sourceMappingURL=architecture-regression.test.js.map