Files
ForgeFlow/tests/security-validation.test.mjs
NuklearRabbit d47c7b5e41
ForgeFlow quality gate / secret-scan (push) Failing after 32s
ForgeFlow quality gate / quality (push) Failing after 0s
feat: add safe Gitea sync and signed updates
2026-08-27 00:38:58 +02:00

69 lines
3.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import validation from '../src/shared/validation.cjs';
import redaction from '../src/main/log-redaction.cjs';
const {
normalizeBaseUrl,
assertRepositoryRelativePath,
assertRepositoryRelativePaths,
assertFullCommitSha,
assertWorkflowFile,
assertWorkflowFileName,
assertBranchName,
assertEnvironmentName,
assertHttpUrl,
assertCloneRemote
} = validation;
const { redactSecrets } = redaction;
test('rejects credentials embedded in service URLs', () => {
assert.throws(() => normalizeBaseUrl(`https://${['jens', 'secret'].join(':')}@gitea.example.test`), /credentials/i);
assert.throws(() => normalizeBaseUrl('http://gitea.example.test'), /must use HTTPS/i);
assert.equal(normalizeBaseUrl('http://127.0.0.1:3000/'), 'http://127.0.0.1:3000');
assert.throws(() => assertHttpUrl(`https://${['user', 'secret'].join(':')}@app.example.test/health`), /credentials/i);
});
test('accepts repository-relative paths but blocks escapes and absolute paths', () => {
assert.equal(assertRepositoryRelativePath('./src/main.ts'), 'src/main.ts');
assert.deepEqual(assertRepositoryRelativePaths(['src/main.ts', 'src/main.ts', 'docs/readme.md']), ['src/main.ts', 'docs/readme.md']);
assert.throws(() => assertRepositoryRelativePath('../secrets.txt'), /escape/i);
assert.throws(() => assertRepositoryRelativePath('/etc/passwd'), /absolute/i);
assert.throws(() => assertRepositoryRelativePath('C:\\Windows\\win.ini'), /absolute/i);
});
test('validates full commit SHAs and workflow filenames', () => {
const sha = 'A'.repeat(40);
assert.equal(assertFullCommitSha(sha), 'a'.repeat(40));
assert.equal(assertWorkflowFile('.gitea/workflows/deploy.yml'), '.gitea/workflows/deploy.yml');
assert.throws(() => assertFullCommitSha('abc1234'), /full commit SHA/i);
assert.throws(() => assertWorkflowFile('../deploy.yml'), /escape/i);
assert.throws(() => assertWorkflowFile('deploy.sh'), /YAML/i);
assert.equal(assertWorkflowFileName('deploy.yml'), 'deploy.yml');
assert.throws(() => assertWorkflowFileName('.gitea/workflows/deploy.yml'), /filename/i);
});
test('allows supported Git remotes and rejects unsafe protocols/passwords', () => {
assert.equal(assertCloneRemote('git@gitea.example.test:jens/app.git'), 'git@gitea.example.test:jens/app.git');
assert.equal(assertCloneRemote('ssh://git@gitea.example.test/jens/app.git'), 'ssh://git@gitea.example.test/jens/app.git');
assert.throws(() => assertCloneRemote('file:///tmp/repo.git'), /unsupported/i);
assert.throws(() => assertCloneRemote(`https://${['jens', 'secret'].join(':')}@gitea.example.test/jens/app.git`), /password/i);
});
test('redacts known tokens, authorization headers, query tokens and URL passwords', () => {
const token = 'super-secret-token';
const source = `Authorization: token ${token}\nhttps://gitea.test/api?access_token=${token}\nhttps://${['jens', 'password'].join(':')}@gitea.test\n${token}`;
const result = redactSecrets(source, [token]);
assert.doesNotMatch(result, /super-secret-token|password/);
assert.match(result, /\[REDACTED\]/);
});
test('validates deployment branch and environment identifiers', () => {
assert.equal(assertBranchName('release/staging'), 'release/staging');
assert.equal(assertEnvironmentName('Production-EU'), 'production-eu');
assert.throws(() => assertBranchName('-dangerous'), /invalid/i);
assert.throws(() => assertBranchName('main..backup'), /invalid/i);
assert.throws(() => assertEnvironmentName('production eu'), /environment/i);
});