tlc-claude-code 1.4.7 → 1.4.9
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/docker-compose.dev.yml +6 -3
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Mapper Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { createControlMapper, mapControl, findOverlaps, generateCrossReference, importFramework } from './control-mapper.js';
|
|
6
|
+
|
|
7
|
+
describe('control-mapper', () => {
|
|
8
|
+
describe('createControlMapper', () => {
|
|
9
|
+
it('creates control mapper', () => {
|
|
10
|
+
const mapper = createControlMapper();
|
|
11
|
+
expect(mapper.map).toBeDefined();
|
|
12
|
+
expect(mapper.findOverlaps).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('loads default mappings', () => {
|
|
16
|
+
const mapper = createControlMapper({ loadDefaults: true });
|
|
17
|
+
const mappings = mapper.getMappings();
|
|
18
|
+
expect(mappings.length).toBeGreaterThan(0);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('mapControl', () => {
|
|
23
|
+
it('maps control between frameworks', () => {
|
|
24
|
+
const mapping = mapControl({
|
|
25
|
+
source: { framework: 'pci-dss', control: 'req-3.4' },
|
|
26
|
+
target: { framework: 'iso27001' }
|
|
27
|
+
});
|
|
28
|
+
expect(mapping.targetControls.length).toBeGreaterThan(0);
|
|
29
|
+
expect(mapping.targetControls.some(c => c.startsWith('A.8'))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('provides mapping confidence', () => {
|
|
33
|
+
const mapping = mapControl({
|
|
34
|
+
source: { framework: 'pci-dss', control: 'req-1.1' },
|
|
35
|
+
target: { framework: 'iso27001' }
|
|
36
|
+
});
|
|
37
|
+
expect(mapping.confidence).toBeDefined();
|
|
38
|
+
expect(mapping.confidence).toBeGreaterThan(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('handles unmapped controls', () => {
|
|
42
|
+
const mapping = mapControl({
|
|
43
|
+
source: { framework: 'custom', control: 'unknown' },
|
|
44
|
+
target: { framework: 'iso27001' }
|
|
45
|
+
});
|
|
46
|
+
expect(mapping.targetControls).toHaveLength(0);
|
|
47
|
+
expect(mapping.unmapped).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('findOverlaps', () => {
|
|
52
|
+
it('finds overlapping controls', () => {
|
|
53
|
+
const overlaps = findOverlaps(['pci-dss', 'iso27001', 'hipaa']);
|
|
54
|
+
expect(overlaps.length).toBeGreaterThan(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('identifies common control themes', () => {
|
|
58
|
+
const overlaps = findOverlaps(['pci-dss', 'hipaa']);
|
|
59
|
+
expect(overlaps.some(o => o.theme === 'encryption')).toBe(true);
|
|
60
|
+
expect(overlaps.some(o => o.theme === 'access-control')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('calculates overlap percentage', () => {
|
|
64
|
+
const overlaps = findOverlaps(['pci-dss', 'iso27001']);
|
|
65
|
+
expect(overlaps[0].overlapPercentage).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('generateCrossReference', () => {
|
|
70
|
+
it('generates cross-reference matrix', () => {
|
|
71
|
+
const matrix = generateCrossReference(['pci-dss', 'iso27001', 'hipaa']);
|
|
72
|
+
expect(matrix['pci-dss']).toBeDefined();
|
|
73
|
+
expect(matrix['pci-dss']['iso27001']).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('outputs as markdown table', () => {
|
|
77
|
+
const markdown = generateCrossReference(['pci-dss', 'iso27001'], { format: 'markdown' });
|
|
78
|
+
expect(markdown).toContain('|');
|
|
79
|
+
expect(markdown).toContain('PCI DSS');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('outputs as CSV', () => {
|
|
83
|
+
const csv = generateCrossReference(['pci-dss', 'iso27001'], { format: 'csv' });
|
|
84
|
+
expect(csv).toContain(',');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('importFramework', () => {
|
|
89
|
+
it('imports custom framework', () => {
|
|
90
|
+
const framework = {
|
|
91
|
+
id: 'custom-framework',
|
|
92
|
+
name: 'Custom Framework',
|
|
93
|
+
controls: [
|
|
94
|
+
{ id: 'CF-1', name: 'Access Control', mappings: ['iso27001:A.5.15'] }
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
const result = importFramework(framework);
|
|
98
|
+
expect(result.imported).toBe(true);
|
|
99
|
+
expect(result.controlCount).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('validates framework structure', () => {
|
|
103
|
+
const invalidFramework = { controls: [] };
|
|
104
|
+
expect(() => importFramework(invalidFramework)).toThrow(/id.*required/i);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('auto-maps to existing frameworks', () => {
|
|
108
|
+
const framework = {
|
|
109
|
+
id: 'custom',
|
|
110
|
+
name: 'Custom',
|
|
111
|
+
controls: [{ id: 'C-1', keywords: ['encryption', 'data protection'] }]
|
|
112
|
+
};
|
|
113
|
+
const result = importFramework(framework, { autoMap: true });
|
|
114
|
+
expect(result.mappingsCreated).toBeGreaterThan(0);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Linker - Evidence linking to controls
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
|
|
7
|
+
// Supported evidence types
|
|
8
|
+
const SUPPORTED_TYPES = ['code', 'config', 'test', 'document', 'screenshot', 'log'];
|
|
9
|
+
|
|
10
|
+
// Compliance annotation patterns
|
|
11
|
+
const ANNOTATION_PATTERNS = [
|
|
12
|
+
{ regex: /@compliance\s+([^\s]+)/i, group: 1 },
|
|
13
|
+
{ regex: /@control\s+([^\s]+)/i, group: 1 },
|
|
14
|
+
{ regex: /@evidence\s+(.+)/i, group: 1, isDescription: true }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Implicit evidence patterns
|
|
18
|
+
const IMPLICIT_PATTERNS = [
|
|
19
|
+
{ pattern: /crypto\.create(?:Cipher|Decipher|Hash)/i, theme: 'encryption' },
|
|
20
|
+
{ pattern: /aes-\d+-[a-z]+/i, theme: 'encryption' },
|
|
21
|
+
{ pattern: /createCipheriv/i, theme: 'encryption' },
|
|
22
|
+
{ pattern: /bcrypt|argon2|scrypt/i, theme: 'encryption' },
|
|
23
|
+
{ pattern: /jwt|jsonwebtoken|oauth/i, theme: 'authentication' },
|
|
24
|
+
{ pattern: /authenticate|login|session/i, theme: 'authentication' },
|
|
25
|
+
{ pattern: /authorize|permission|role/i, theme: 'access-control' },
|
|
26
|
+
{ pattern: /audit|log.*event/i, theme: 'logging' },
|
|
27
|
+
{ pattern: /firewall|iptables|security.?group/i, theme: 'network-security' }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Evidence storage
|
|
31
|
+
const linkedEvidence = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create an evidence linker instance
|
|
35
|
+
*/
|
|
36
|
+
export function createEvidenceLinker() {
|
|
37
|
+
return {
|
|
38
|
+
link: (evidence) => linkEvidence(evidence),
|
|
39
|
+
scan: (options) => scanCodebase(options),
|
|
40
|
+
getSupportedTypes: () => [...SUPPORTED_TYPES],
|
|
41
|
+
getLinkedEvidence: () => [...linkedEvidence.values()]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Link evidence to a control
|
|
47
|
+
*/
|
|
48
|
+
export function linkEvidence(evidence) {
|
|
49
|
+
const result = {
|
|
50
|
+
linked: false,
|
|
51
|
+
evidenceId: null,
|
|
52
|
+
errors: []
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Validate required fields
|
|
56
|
+
if (!evidence.controlId) {
|
|
57
|
+
result.errors.push('controlId is required');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!evidence.type && !evidence.file) {
|
|
61
|
+
result.errors.push('type or file is required');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (evidence.type && !SUPPORTED_TYPES.includes(evidence.type)) {
|
|
65
|
+
result.errors.push(`Invalid evidence type: ${evidence.type}. Supported: ${SUPPORTED_TYPES.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If there are validation errors, return early
|
|
69
|
+
if (result.errors.length > 0) {
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Generate evidence ID and store
|
|
74
|
+
const evidenceId = randomUUID();
|
|
75
|
+
const evidenceRecord = {
|
|
76
|
+
id: evidenceId,
|
|
77
|
+
controlId: evidence.controlId,
|
|
78
|
+
type: evidence.type || 'code',
|
|
79
|
+
file: evidence.file,
|
|
80
|
+
lines: evidence.lines,
|
|
81
|
+
description: evidence.description,
|
|
82
|
+
linkedAt: new Date().toISOString()
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
linkedEvidence.set(evidenceId, evidenceRecord);
|
|
86
|
+
|
|
87
|
+
result.linked = true;
|
|
88
|
+
result.evidenceId = evidenceId;
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Scan codebase for compliance evidence
|
|
95
|
+
*/
|
|
96
|
+
export async function scanCodebase(options = {}) {
|
|
97
|
+
const { glob, readFile, detectPatterns = false } = options;
|
|
98
|
+
const results = [];
|
|
99
|
+
|
|
100
|
+
if (!glob || !readFile) {
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const files = await glob('**/*.{js,ts,jsx,tsx,py,go,java}');
|
|
106
|
+
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
try {
|
|
109
|
+
const content = await readFile(file);
|
|
110
|
+
const lines = content.split('\n');
|
|
111
|
+
|
|
112
|
+
// Scan for explicit annotations
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
const line = lines[i];
|
|
115
|
+
|
|
116
|
+
for (const annotationPattern of ANNOTATION_PATTERNS) {
|
|
117
|
+
const match = line.match(annotationPattern.regex);
|
|
118
|
+
if (match) {
|
|
119
|
+
if (annotationPattern.isDescription) {
|
|
120
|
+
// @evidence annotation - attach to previous result
|
|
121
|
+
if (results.length > 0) {
|
|
122
|
+
results[results.length - 1].description = match[annotationPattern.group];
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
results.push({
|
|
126
|
+
file,
|
|
127
|
+
line: i + 1,
|
|
128
|
+
controlId: match[annotationPattern.group],
|
|
129
|
+
type: 'annotation'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Scan for implicit patterns if requested
|
|
137
|
+
if (detectPatterns) {
|
|
138
|
+
for (const implicitPattern of IMPLICIT_PATTERNS) {
|
|
139
|
+
if (implicitPattern.pattern.test(content)) {
|
|
140
|
+
results.push({
|
|
141
|
+
file,
|
|
142
|
+
pattern: implicitPattern.theme,
|
|
143
|
+
type: 'implicit',
|
|
144
|
+
confidence: 0.7
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// Skip unreadable files
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
// Return empty results on glob error
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate an evidence report for a framework
|
|
162
|
+
*/
|
|
163
|
+
export function generateEvidenceReport(evidence, options = {}) {
|
|
164
|
+
const { framework, showGaps = false } = options;
|
|
165
|
+
|
|
166
|
+
let report = `# Evidence Report\n\n`;
|
|
167
|
+
|
|
168
|
+
if (framework) {
|
|
169
|
+
const frameworkNames = {
|
|
170
|
+
'pci-dss': 'PCI DSS',
|
|
171
|
+
'iso27001': 'ISO 27001',
|
|
172
|
+
'hipaa': 'HIPAA'
|
|
173
|
+
};
|
|
174
|
+
report += `**Framework:** ${frameworkNames[framework] || framework}\n\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
report += `## Linked Evidence\n\n`;
|
|
178
|
+
|
|
179
|
+
if (evidence.length === 0) {
|
|
180
|
+
report += `No evidence linked.\n\n`;
|
|
181
|
+
} else {
|
|
182
|
+
report += `| Control | Type | File | Description |\n`;
|
|
183
|
+
report += `|---------|------|------|-------------|\n`;
|
|
184
|
+
|
|
185
|
+
for (const e of evidence) {
|
|
186
|
+
report += `| ${e.controlId} | ${e.type || '-'} | ${e.file || '-'} | ${e.description || '-'} |\n`;
|
|
187
|
+
}
|
|
188
|
+
report += '\n';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (showGaps) {
|
|
192
|
+
report += `## Missing Evidence\n\n`;
|
|
193
|
+
report += `The following controls are missing evidence documentation:\n\n`;
|
|
194
|
+
|
|
195
|
+
// Get framework controls that don't have evidence
|
|
196
|
+
const coveredControls = new Set(evidence.map(e => e.controlId));
|
|
197
|
+
const frameworkControls = getFrameworkControls(framework);
|
|
198
|
+
const missingControls = frameworkControls.filter(c => !coveredControls.has(c));
|
|
199
|
+
|
|
200
|
+
if (missingControls.length > 0) {
|
|
201
|
+
for (const control of missingControls) {
|
|
202
|
+
report += `- ${control}\n`;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
report += `All controls have evidence linked.\n`;
|
|
206
|
+
}
|
|
207
|
+
report += '\n';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return report;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get controls for a framework
|
|
215
|
+
*/
|
|
216
|
+
function getFrameworkControls(framework) {
|
|
217
|
+
const controls = {
|
|
218
|
+
'pci-dss': [
|
|
219
|
+
'req-1.1', 'req-1.2', 'req-1.3',
|
|
220
|
+
'req-2.1', 'req-2.2',
|
|
221
|
+
'req-3.1', 'req-3.2', 'req-3.4',
|
|
222
|
+
'req-4.1',
|
|
223
|
+
'req-5.1', 'req-5.2',
|
|
224
|
+
'req-6.1', 'req-6.2',
|
|
225
|
+
'req-7.1', 'req-7.2',
|
|
226
|
+
'req-8.1', 'req-8.2',
|
|
227
|
+
'req-9.1',
|
|
228
|
+
'req-10.1', 'req-10.2', 'req-10.3',
|
|
229
|
+
'req-11.1', 'req-11.2',
|
|
230
|
+
'req-12.1'
|
|
231
|
+
],
|
|
232
|
+
'iso27001': [
|
|
233
|
+
'A.5.1', 'A.5.15', 'A.5.16', 'A.5.17',
|
|
234
|
+
'A.6.1', 'A.6.2',
|
|
235
|
+
'A.7.1', 'A.7.2',
|
|
236
|
+
'A.8.1', 'A.8.15', 'A.8.24',
|
|
237
|
+
'A.9.1',
|
|
238
|
+
'A.10.1',
|
|
239
|
+
'A.11.1',
|
|
240
|
+
'A.12.1',
|
|
241
|
+
'A.13.1', 'A.13.2'
|
|
242
|
+
],
|
|
243
|
+
'hipaa': [
|
|
244
|
+
'164.312(a)(1)', '164.312(a)(2)(iv)',
|
|
245
|
+
'164.312(b)',
|
|
246
|
+
'164.312(c)(1)',
|
|
247
|
+
'164.312(d)',
|
|
248
|
+
'164.312(e)(1)', '164.312(e)(2)(ii)'
|
|
249
|
+
]
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return controls[framework] || [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate evidence
|
|
257
|
+
* Returns a Promise if checkExists is used with an async exists function,
|
|
258
|
+
* otherwise returns the result synchronously
|
|
259
|
+
*/
|
|
260
|
+
export function validateEvidence(evidence, options = {}) {
|
|
261
|
+
const result = {
|
|
262
|
+
valid: true,
|
|
263
|
+
issues: []
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Check freshness
|
|
267
|
+
if (options.maxAge && evidence.lastModified) {
|
|
268
|
+
const age = (Date.now() - new Date(evidence.lastModified).getTime()) / (24 * 60 * 60 * 1000);
|
|
269
|
+
if (age > options.maxAge) {
|
|
270
|
+
result.valid = false;
|
|
271
|
+
result.issues.push('stale');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check file exists - this is async
|
|
276
|
+
if (options.checkExists && evidence.file) {
|
|
277
|
+
const existsCheck = options.exists ? options.exists(evidence.file) : Promise.resolve(true);
|
|
278
|
+
return Promise.resolve(existsCheck).then(exists => {
|
|
279
|
+
if (!exists) {
|
|
280
|
+
result.valid = false;
|
|
281
|
+
result.issues.push('file-not-found');
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default {
|
|
291
|
+
createEvidenceLinker,
|
|
292
|
+
linkEvidence,
|
|
293
|
+
scanCodebase,
|
|
294
|
+
generateEvidenceReport,
|
|
295
|
+
validateEvidence
|
|
296
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Linker Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { createEvidenceLinker, linkEvidence, scanCodebase, generateEvidenceReport, validateEvidence } from './evidence-linker.js';
|
|
6
|
+
|
|
7
|
+
describe('evidence-linker', () => {
|
|
8
|
+
describe('createEvidenceLinker', () => {
|
|
9
|
+
it('creates evidence linker', () => {
|
|
10
|
+
const linker = createEvidenceLinker();
|
|
11
|
+
expect(linker.link).toBeDefined();
|
|
12
|
+
expect(linker.scan).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('supports multiple evidence types', () => {
|
|
16
|
+
const linker = createEvidenceLinker();
|
|
17
|
+
const types = linker.getSupportedTypes();
|
|
18
|
+
expect(types).toContain('code');
|
|
19
|
+
expect(types).toContain('config');
|
|
20
|
+
expect(types).toContain('test');
|
|
21
|
+
expect(types).toContain('document');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('linkEvidence', () => {
|
|
26
|
+
it('links code to control', () => {
|
|
27
|
+
const evidence = {
|
|
28
|
+
controlId: 'pci-dss:req-3.4',
|
|
29
|
+
type: 'code',
|
|
30
|
+
file: 'src/encryption.js',
|
|
31
|
+
lines: '10-25',
|
|
32
|
+
description: 'AES-256 encryption implementation'
|
|
33
|
+
};
|
|
34
|
+
const result = linkEvidence(evidence);
|
|
35
|
+
expect(result.linked).toBe(true);
|
|
36
|
+
expect(result.evidenceId).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('links test to control', () => {
|
|
40
|
+
const evidence = {
|
|
41
|
+
controlId: 'iso27001:A.8.24',
|
|
42
|
+
type: 'test',
|
|
43
|
+
file: 'tests/encryption.test.js',
|
|
44
|
+
description: 'Encryption tests'
|
|
45
|
+
};
|
|
46
|
+
const result = linkEvidence(evidence);
|
|
47
|
+
expect(result.linked).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('validates evidence completeness', () => {
|
|
51
|
+
const evidence = { controlId: 'test' };
|
|
52
|
+
const result = linkEvidence(evidence);
|
|
53
|
+
expect(result.linked).toBe(false);
|
|
54
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('scanCodebase', () => {
|
|
59
|
+
it('scans codebase for evidence', async () => {
|
|
60
|
+
const mockGlob = vi.fn().mockResolvedValue(['src/auth.js', 'src/encryption.js']);
|
|
61
|
+
const mockRead = vi.fn().mockResolvedValue('// @compliance pci-dss:req-8.1\nfunction authenticate() {}');
|
|
62
|
+
|
|
63
|
+
const results = await scanCodebase({ glob: mockGlob, readFile: mockRead });
|
|
64
|
+
expect(results.length).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('identifies compliance annotations', async () => {
|
|
68
|
+
const mockGlob = vi.fn().mockResolvedValue(['test.js']);
|
|
69
|
+
const mockRead = vi.fn().mockResolvedValue('// @control iso27001:A.5.15\n// @evidence access control implementation');
|
|
70
|
+
|
|
71
|
+
const results = await scanCodebase({ glob: mockGlob, readFile: mockRead });
|
|
72
|
+
expect(results[0].controlId).toBe('iso27001:A.5.15');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('detects implicit evidence patterns', async () => {
|
|
76
|
+
const mockGlob = vi.fn().mockResolvedValue(['src/crypto.js']);
|
|
77
|
+
const mockRead = vi.fn().mockResolvedValue('crypto.createCipheriv("aes-256-gcm", key, iv)');
|
|
78
|
+
|
|
79
|
+
const results = await scanCodebase({ glob: mockGlob, readFile: mockRead, detectPatterns: true });
|
|
80
|
+
expect(results.some(r => r.pattern === 'encryption')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('generateEvidenceReport', () => {
|
|
85
|
+
it('generates evidence report for framework', () => {
|
|
86
|
+
const evidence = [
|
|
87
|
+
{ controlId: 'req-1.1', type: 'code', file: 'firewall.js' },
|
|
88
|
+
{ controlId: 'req-3.4', type: 'test', file: 'encryption.test.js' }
|
|
89
|
+
];
|
|
90
|
+
const report = generateEvidenceReport(evidence, { framework: 'pci-dss' });
|
|
91
|
+
expect(report).toContain('Evidence Report');
|
|
92
|
+
expect(report).toContain('req-1.1');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('identifies controls without evidence', () => {
|
|
96
|
+
const evidence = [{ controlId: 'req-1.1', type: 'code', file: 'test.js' }];
|
|
97
|
+
const report = generateEvidenceReport(evidence, { framework: 'pci-dss', showGaps: true });
|
|
98
|
+
expect(report).toContain('Missing Evidence');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('validateEvidence', () => {
|
|
103
|
+
it('validates evidence freshness', () => {
|
|
104
|
+
const evidence = {
|
|
105
|
+
controlId: 'test',
|
|
106
|
+
file: 'test.js',
|
|
107
|
+
lastModified: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)
|
|
108
|
+
};
|
|
109
|
+
const result = validateEvidence(evidence, { maxAge: 90 });
|
|
110
|
+
expect(result.valid).toBe(false);
|
|
111
|
+
expect(result.issues).toContain('stale');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('validates evidence file exists', async () => {
|
|
115
|
+
const mockExists = vi.fn().mockResolvedValue(false);
|
|
116
|
+
const evidence = { controlId: 'test', file: 'deleted.js' };
|
|
117
|
+
const result = await validateEvidence(evidence, { checkExists: true, exists: mockExists });
|
|
118
|
+
expect(result.valid).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|