@grc-claw/evidence-collector 0.8.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.
- package/dist/EvidenceCollector.d.ts +39 -0
- package/dist/EvidenceCollector.js +95 -0
- package/dist/collectors/AccessControlCollector.d.ts +8 -0
- package/dist/collectors/AccessControlCollector.js +48 -0
- package/dist/collectors/BackupCollector.d.ts +8 -0
- package/dist/collectors/BackupCollector.js +49 -0
- package/dist/collectors/EncryptionCollector.d.ts +9 -0
- package/dist/collectors/EncryptionCollector.js +63 -0
- package/dist/collectors/LoggingCollector.d.ts +8 -0
- package/dist/collectors/LoggingCollector.js +48 -0
- package/dist/collectors/MFACollector.d.ts +8 -0
- package/dist/collectors/MFACollector.js +48 -0
- package/dist/collectors/NetworkSecurityCollector.d.ts +8 -0
- package/dist/collectors/NetworkSecurityCollector.js +44 -0
- package/dist/collectors/PatchManagementCollector.d.ts +8 -0
- package/dist/collectors/PatchManagementCollector.js +46 -0
- package/dist/collectors/index.d.ts +7 -0
- package/dist/collectors/index.js +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +145 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EvidenceItem, EvidenceCategory, ComplianceFramework, SystemAdapter, CollectorStatus } from "./types.js";
|
|
2
|
+
export interface EvidenceCollectionRequest {
|
|
3
|
+
category: EvidenceCategory;
|
|
4
|
+
framework: ComplianceFramework;
|
|
5
|
+
controlId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface EvidenceCollectionResult {
|
|
8
|
+
items: EvidenceItem[];
|
|
9
|
+
status: CollectorStatus;
|
|
10
|
+
collectedAt: string;
|
|
11
|
+
errors: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare class EvidenceCollectorEngine {
|
|
14
|
+
private adapter;
|
|
15
|
+
private mfaCollector;
|
|
16
|
+
private encryptionCollector;
|
|
17
|
+
private accessControlCollector;
|
|
18
|
+
private loggingCollector;
|
|
19
|
+
private patchCollector;
|
|
20
|
+
private networkCollector;
|
|
21
|
+
private backupCollector;
|
|
22
|
+
private evidenceStore;
|
|
23
|
+
constructor(adapter: SystemAdapter);
|
|
24
|
+
collect(requests: EvidenceCollectionRequest[]): Promise<EvidenceCollectionResult>;
|
|
25
|
+
collectSingle(request: EvidenceCollectionRequest): Promise<EvidenceItem>;
|
|
26
|
+
getEvidence(id: string): EvidenceItem | undefined;
|
|
27
|
+
getAllEvidence(): EvidenceItem[];
|
|
28
|
+
getEvidenceByFramework(framework: ComplianceFramework): EvidenceItem[];
|
|
29
|
+
getEvidenceByCategory(category: EvidenceCategory): EvidenceItem[];
|
|
30
|
+
getComplianceSummary(framework: ComplianceFramework): {
|
|
31
|
+
total: number;
|
|
32
|
+
compliant: number;
|
|
33
|
+
nonCompliant: number;
|
|
34
|
+
partial: number;
|
|
35
|
+
unknown: number;
|
|
36
|
+
compliancePercentage: number;
|
|
37
|
+
};
|
|
38
|
+
clearEvidence(): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { MFACollector, EncryptionCollector, AccessControlCollector, LoggingCollector, PatchManagementCollector, NetworkSecurityCollector, BackupCollector, } from "./collectors/index.js";
|
|
2
|
+
export class EvidenceCollectorEngine {
|
|
3
|
+
adapter;
|
|
4
|
+
mfaCollector;
|
|
5
|
+
encryptionCollector;
|
|
6
|
+
accessControlCollector;
|
|
7
|
+
loggingCollector;
|
|
8
|
+
patchCollector;
|
|
9
|
+
networkCollector;
|
|
10
|
+
backupCollector;
|
|
11
|
+
evidenceStore = new Map();
|
|
12
|
+
constructor(adapter) {
|
|
13
|
+
this.adapter = adapter;
|
|
14
|
+
this.mfaCollector = new MFACollector(adapter);
|
|
15
|
+
this.encryptionCollector = new EncryptionCollector(adapter);
|
|
16
|
+
this.accessControlCollector = new AccessControlCollector(adapter);
|
|
17
|
+
this.loggingCollector = new LoggingCollector(adapter);
|
|
18
|
+
this.patchCollector = new PatchManagementCollector(adapter);
|
|
19
|
+
this.networkCollector = new NetworkSecurityCollector(adapter);
|
|
20
|
+
this.backupCollector = new BackupCollector(adapter);
|
|
21
|
+
}
|
|
22
|
+
async collect(requests) {
|
|
23
|
+
const items = [];
|
|
24
|
+
const errors = [];
|
|
25
|
+
for (const request of requests) {
|
|
26
|
+
try {
|
|
27
|
+
const item = await this.collectSingle(request);
|
|
28
|
+
items.push(item);
|
|
29
|
+
this.evidenceStore.set(item.id, item);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
errors.push(`Failed to collect ${request.category} for ${request.controlId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
items,
|
|
37
|
+
status: errors.length > 0 ? (items.length > 0 ? "completed" : "error") : "completed",
|
|
38
|
+
collectedAt: new Date().toISOString(),
|
|
39
|
+
errors,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async collectSingle(request) {
|
|
43
|
+
const { category, framework, controlId } = request;
|
|
44
|
+
switch (category) {
|
|
45
|
+
case "mfa":
|
|
46
|
+
return this.mfaCollector.collect(framework, controlId);
|
|
47
|
+
case "encryption":
|
|
48
|
+
return this.encryptionCollector.collectAtRest(framework, controlId);
|
|
49
|
+
case "access_control":
|
|
50
|
+
return this.accessControlCollector.collect(framework, controlId);
|
|
51
|
+
case "logging":
|
|
52
|
+
return this.loggingCollector.collect(framework, controlId);
|
|
53
|
+
case "patch_management":
|
|
54
|
+
return this.patchCollector.collect(framework, controlId);
|
|
55
|
+
case "network_security":
|
|
56
|
+
return this.networkCollector.collect(framework, controlId);
|
|
57
|
+
case "backup":
|
|
58
|
+
return this.backupCollector.collect(framework, controlId);
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Unknown evidence category: ${category}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
getEvidence(id) {
|
|
64
|
+
return this.evidenceStore.get(id);
|
|
65
|
+
}
|
|
66
|
+
getAllEvidence() {
|
|
67
|
+
return Array.from(this.evidenceStore.values());
|
|
68
|
+
}
|
|
69
|
+
getEvidenceByFramework(framework) {
|
|
70
|
+
return this.getAllEvidence().filter((e) => e.framework === framework);
|
|
71
|
+
}
|
|
72
|
+
getEvidenceByCategory(category) {
|
|
73
|
+
return this.getAllEvidence().filter((e) => e.category === category);
|
|
74
|
+
}
|
|
75
|
+
getComplianceSummary(framework) {
|
|
76
|
+
const evidence = this.getEvidenceByFramework(framework);
|
|
77
|
+
const compliant = evidence.filter((e) => e.status === "compliant").length;
|
|
78
|
+
const nonCompliant = evidence.filter((e) => e.status === "non_compliant").length;
|
|
79
|
+
const partial = evidence.filter((e) => e.status === "partial").length;
|
|
80
|
+
const unknown = evidence.filter((e) => e.status === "unknown").length;
|
|
81
|
+
return {
|
|
82
|
+
total: evidence.length,
|
|
83
|
+
compliant,
|
|
84
|
+
nonCompliant,
|
|
85
|
+
partial,
|
|
86
|
+
unknown,
|
|
87
|
+
compliancePercentage: evidence.length > 0
|
|
88
|
+
? Math.round((compliant / evidence.length) * 100)
|
|
89
|
+
: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
clearEvidence() {
|
|
93
|
+
this.evidenceStore.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceItem, ComplianceFramework, SystemAdapter } from "../types.js";
|
|
2
|
+
export declare class AccessControlCollector {
|
|
3
|
+
private adapter;
|
|
4
|
+
constructor(adapter: SystemAdapter);
|
|
5
|
+
collect(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
6
|
+
private computeHash;
|
|
7
|
+
private determineStatus;
|
|
8
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export class AccessControlCollector {
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
async collect(framework, controlId) {
|
|
8
|
+
const data = await this.adapter.queryAccessControl();
|
|
9
|
+
const evidenceData = {
|
|
10
|
+
leastPrivilege: data.leastPrivilege,
|
|
11
|
+
totalRoles: data.totalRoles,
|
|
12
|
+
excessiveRoles: data.excessiveRoles,
|
|
13
|
+
lastAuditAt: data.lastAuditAt,
|
|
14
|
+
excessiveRolePercentage: data.totalRoles > 0
|
|
15
|
+
? Math.round((data.excessiveRoles / data.totalRoles) * 100)
|
|
16
|
+
: 0,
|
|
17
|
+
details: data.details,
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
id: randomUUID(),
|
|
21
|
+
category: "access_control",
|
|
22
|
+
controlId,
|
|
23
|
+
framework,
|
|
24
|
+
source: "iam-api",
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
hash: this.computeHash(evidenceData),
|
|
27
|
+
data: evidenceData,
|
|
28
|
+
status: this.determineStatus(data),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
computeHash(data) {
|
|
32
|
+
const payload = JSON.stringify(data);
|
|
33
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
34
|
+
return `sha256:${hash}`;
|
|
35
|
+
}
|
|
36
|
+
determineStatus(data) {
|
|
37
|
+
if (!data.leastPrivilege)
|
|
38
|
+
return "non_compliant";
|
|
39
|
+
if (data.excessiveRoles === 0)
|
|
40
|
+
return "compliant";
|
|
41
|
+
if (data.totalRoles > 0) {
|
|
42
|
+
const ratio = data.excessiveRoles / data.totalRoles;
|
|
43
|
+
if (ratio <= 0.1)
|
|
44
|
+
return "partial";
|
|
45
|
+
}
|
|
46
|
+
return "non_compliant";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceItem, ComplianceFramework, SystemAdapter } from "../types.js";
|
|
2
|
+
export declare class BackupCollector {
|
|
3
|
+
private adapter;
|
|
4
|
+
constructor(adapter: SystemAdapter);
|
|
5
|
+
collect(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
6
|
+
private computeHash;
|
|
7
|
+
private determineStatus;
|
|
8
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export class BackupCollector {
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
async collect(framework, controlId) {
|
|
8
|
+
const data = await this.adapter.queryBackup();
|
|
9
|
+
const evidenceData = {
|
|
10
|
+
configured: data.configured,
|
|
11
|
+
frequency: data.frequency,
|
|
12
|
+
lastBackupAt: data.lastBackupAt,
|
|
13
|
+
retentionDays: data.retentionDays,
|
|
14
|
+
testedAt: data.testedAt,
|
|
15
|
+
testPassed: data.testPassed,
|
|
16
|
+
daysSinceLastBackup: data.lastBackupAt
|
|
17
|
+
? Math.floor((Date.now() - new Date(data.lastBackupAt).getTime()) /
|
|
18
|
+
(1000 * 60 * 60 * 24))
|
|
19
|
+
: null,
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
id: randomUUID(),
|
|
23
|
+
category: "backup",
|
|
24
|
+
controlId,
|
|
25
|
+
framework,
|
|
26
|
+
source: "backup-api",
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
hash: this.computeHash(evidenceData),
|
|
29
|
+
data: evidenceData,
|
|
30
|
+
status: this.determineStatus(data),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
computeHash(data) {
|
|
34
|
+
const payload = JSON.stringify(data);
|
|
35
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
36
|
+
return `sha256:${hash}`;
|
|
37
|
+
}
|
|
38
|
+
determineStatus(data) {
|
|
39
|
+
if (!data.configured)
|
|
40
|
+
return "non_compliant";
|
|
41
|
+
if (!data.testPassed)
|
|
42
|
+
return "non_compliant";
|
|
43
|
+
if (data.retentionDays < 30)
|
|
44
|
+
return "partial";
|
|
45
|
+
if (data.configured && data.testPassed && data.retentionDays >= 90)
|
|
46
|
+
return "compliant";
|
|
47
|
+
return "partial";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EvidenceItem, ComplianceFramework, SystemAdapter } from "../types.js";
|
|
2
|
+
export declare class EncryptionCollector {
|
|
3
|
+
private adapter;
|
|
4
|
+
constructor(adapter: SystemAdapter);
|
|
5
|
+
collectAtRest(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
6
|
+
collectInTransit(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
7
|
+
private computeHash;
|
|
8
|
+
private determineStatus;
|
|
9
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export class EncryptionCollector {
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
async collectAtRest(framework, controlId) {
|
|
8
|
+
const data = await this.adapter.queryEncryptionAtRest();
|
|
9
|
+
const evidenceData = {
|
|
10
|
+
atRest: true,
|
|
11
|
+
enabled: data.enabled,
|
|
12
|
+
algorithm: data.algorithm,
|
|
13
|
+
keyRotationDays: data.keyRotationDays,
|
|
14
|
+
lastRotatedAt: data.lastRotatedAt,
|
|
15
|
+
details: data.details,
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
id: randomUUID(),
|
|
19
|
+
category: "encryption",
|
|
20
|
+
controlId,
|
|
21
|
+
framework,
|
|
22
|
+
source: "cloud-api",
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
hash: this.computeHash(evidenceData),
|
|
25
|
+
data: evidenceData,
|
|
26
|
+
status: this.determineStatus(data),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async collectInTransit(framework, controlId) {
|
|
30
|
+
const data = await this.adapter.queryEncryptionInTransit();
|
|
31
|
+
const evidenceData = {
|
|
32
|
+
inTransit: true,
|
|
33
|
+
enabled: data.enabled,
|
|
34
|
+
algorithm: data.algorithm,
|
|
35
|
+
details: data.details,
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
id: randomUUID(),
|
|
39
|
+
category: "encryption",
|
|
40
|
+
controlId,
|
|
41
|
+
framework,
|
|
42
|
+
source: "network-api",
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
hash: this.computeHash(evidenceData),
|
|
45
|
+
data: evidenceData,
|
|
46
|
+
status: this.determineStatus(data),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
computeHash(data) {
|
|
50
|
+
const payload = JSON.stringify(data);
|
|
51
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
52
|
+
return `sha256:${hash}`;
|
|
53
|
+
}
|
|
54
|
+
determineStatus(data) {
|
|
55
|
+
if (!data.enabled)
|
|
56
|
+
return "non_compliant";
|
|
57
|
+
if (data.algorithm && data.keyRotationDays)
|
|
58
|
+
return "compliant";
|
|
59
|
+
if (data.algorithm || data.keyRotationDays)
|
|
60
|
+
return "partial";
|
|
61
|
+
return "compliant";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceItem, ComplianceFramework, SystemAdapter } from "../types.js";
|
|
2
|
+
export declare class LoggingCollector {
|
|
3
|
+
private adapter;
|
|
4
|
+
constructor(adapter: SystemAdapter);
|
|
5
|
+
collect(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
6
|
+
private computeHash;
|
|
7
|
+
private determineStatus;
|
|
8
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export class LoggingCollector {
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
async collect(framework, controlId) {
|
|
8
|
+
const data = await this.adapter.queryLogging();
|
|
9
|
+
const evidenceData = {
|
|
10
|
+
enabled: data.enabled,
|
|
11
|
+
logTypes: data.logTypes,
|
|
12
|
+
retentionDays: data.retentionDays,
|
|
13
|
+
alertingEnabled: data.alertingEnabled,
|
|
14
|
+
lastConfiguredAt: data.lastConfiguredAt,
|
|
15
|
+
hasAuditLogs: data.logTypes.includes("audit"),
|
|
16
|
+
hasSecurityLogs: data.logTypes.includes("security"),
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
id: randomUUID(),
|
|
20
|
+
category: "logging",
|
|
21
|
+
controlId,
|
|
22
|
+
framework,
|
|
23
|
+
source: "logging-api",
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
hash: this.computeHash(evidenceData),
|
|
26
|
+
data: evidenceData,
|
|
27
|
+
status: this.determineStatus(data),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
computeHash(data) {
|
|
31
|
+
const payload = JSON.stringify(data);
|
|
32
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
33
|
+
return `sha256:${hash}`;
|
|
34
|
+
}
|
|
35
|
+
determineStatus(data) {
|
|
36
|
+
if (!data.enabled)
|
|
37
|
+
return "non_compliant";
|
|
38
|
+
if (data.retentionDays < 90)
|
|
39
|
+
return "non_compliant";
|
|
40
|
+
const hasAudit = data.logTypes.includes("audit");
|
|
41
|
+
const hasSecurity = data.logTypes.includes("security");
|
|
42
|
+
if (hasAudit && hasSecurity && data.alertingEnabled && data.retentionDays >= 365)
|
|
43
|
+
return "compliant";
|
|
44
|
+
if (hasAudit || hasSecurity)
|
|
45
|
+
return "partial";
|
|
46
|
+
return "non_compliant";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceItem, ComplianceFramework, SystemAdapter } from "../types.js";
|
|
2
|
+
export declare class MFACollector {
|
|
3
|
+
private adapter;
|
|
4
|
+
constructor(adapter: SystemAdapter);
|
|
5
|
+
collect(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
6
|
+
private computeHash;
|
|
7
|
+
private determineStatus;
|
|
8
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export class MFACollector {
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
async collect(framework, controlId) {
|
|
8
|
+
const mfaData = await this.adapter.queryMFA();
|
|
9
|
+
const evidenceData = {
|
|
10
|
+
enforced: mfaData.enforced,
|
|
11
|
+
totalUsers: mfaData.totalUsers,
|
|
12
|
+
mfaEnabledUsers: mfaData.mfaEnabledUsers,
|
|
13
|
+
methods: mfaData.methods,
|
|
14
|
+
lastEnforcedAt: mfaData.lastEnforcedAt,
|
|
15
|
+
compliancePercentage: mfaData.totalUsers > 0
|
|
16
|
+
? Math.round((mfaData.mfaEnabledUsers / mfaData.totalUsers) * 100)
|
|
17
|
+
: 0,
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
id: randomUUID(),
|
|
21
|
+
category: "mfa",
|
|
22
|
+
controlId,
|
|
23
|
+
framework,
|
|
24
|
+
source: "idp-api",
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
hash: this.computeHash(evidenceData),
|
|
27
|
+
data: evidenceData,
|
|
28
|
+
status: this.determineStatus(mfaData),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
computeHash(data) {
|
|
32
|
+
const payload = JSON.stringify(data);
|
|
33
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
34
|
+
return `sha256:${hash}`;
|
|
35
|
+
}
|
|
36
|
+
determineStatus(data) {
|
|
37
|
+
if (!data.enforced)
|
|
38
|
+
return "non_compliant";
|
|
39
|
+
if (data.totalUsers === 0)
|
|
40
|
+
return "unknown";
|
|
41
|
+
const ratio = data.mfaEnabledUsers / data.totalUsers;
|
|
42
|
+
if (ratio >= 0.95)
|
|
43
|
+
return "compliant";
|
|
44
|
+
if (ratio >= 0.8)
|
|
45
|
+
return "partial";
|
|
46
|
+
return "non_compliant";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceItem, ComplianceFramework, SystemAdapter } from "../types.js";
|
|
2
|
+
export declare class NetworkSecurityCollector {
|
|
3
|
+
private adapter;
|
|
4
|
+
constructor(adapter: SystemAdapter);
|
|
5
|
+
collect(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
6
|
+
private computeHash;
|
|
7
|
+
private determineStatus;
|
|
8
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export class NetworkSecurityCollector {
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
async collect(framework, controlId) {
|
|
8
|
+
const data = await this.adapter.queryNetworkSecurity();
|
|
9
|
+
const evidenceData = {
|
|
10
|
+
firewallEnabled: data.firewallEnabled,
|
|
11
|
+
segmentationEnabled: data.segmentationEnabled,
|
|
12
|
+
totalRules: data.totalRules,
|
|
13
|
+
openPorts: data.openPorts,
|
|
14
|
+
lastAuditAt: data.lastAuditAt,
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
id: randomUUID(),
|
|
18
|
+
category: "network_security",
|
|
19
|
+
controlId,
|
|
20
|
+
framework,
|
|
21
|
+
source: "network-api",
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
hash: this.computeHash(evidenceData),
|
|
24
|
+
data: evidenceData,
|
|
25
|
+
status: this.determineStatus(data),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
computeHash(data) {
|
|
29
|
+
const payload = JSON.stringify(data);
|
|
30
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
31
|
+
return `sha256:${hash}`;
|
|
32
|
+
}
|
|
33
|
+
determineStatus(data) {
|
|
34
|
+
if (!data.firewallEnabled)
|
|
35
|
+
return "non_compliant";
|
|
36
|
+
if (!data.segmentationEnabled)
|
|
37
|
+
return "partial";
|
|
38
|
+
if (data.openPorts > 10)
|
|
39
|
+
return "partial";
|
|
40
|
+
if (data.firewallEnabled && data.segmentationEnabled && data.openPorts <= 5)
|
|
41
|
+
return "compliant";
|
|
42
|
+
return "partial";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EvidenceItem, ComplianceFramework, SystemAdapter } from "../types.js";
|
|
2
|
+
export declare class PatchManagementCollector {
|
|
3
|
+
private adapter;
|
|
4
|
+
constructor(adapter: SystemAdapter);
|
|
5
|
+
collect(framework: ComplianceFramework, controlId: string): Promise<EvidenceItem>;
|
|
6
|
+
private computeHash;
|
|
7
|
+
private determineStatus;
|
|
8
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
export class PatchManagementCollector {
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
async collect(framework, controlId) {
|
|
8
|
+
const data = await this.adapter.queryPatchManagement();
|
|
9
|
+
const daysSincePatch = Math.floor((Date.now() - new Date(data.lastPatchDate).getTime()) / (1000 * 60 * 60 * 24));
|
|
10
|
+
const evidenceData = {
|
|
11
|
+
lastPatchDate: data.lastPatchDate,
|
|
12
|
+
daysSinceLastPatch: daysSincePatch,
|
|
13
|
+
pendingPatches: data.pendingPatches,
|
|
14
|
+
criticalPatches: data.criticalPatches,
|
|
15
|
+
autoUpdateEnabled: data.autoUpdateEnabled,
|
|
16
|
+
details: data.details,
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
id: randomUUID(),
|
|
20
|
+
category: "patch_management",
|
|
21
|
+
controlId,
|
|
22
|
+
framework,
|
|
23
|
+
source: "patch-api",
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
hash: this.computeHash(evidenceData),
|
|
26
|
+
data: evidenceData,
|
|
27
|
+
status: this.determineStatus(data, daysSincePatch),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
computeHash(data) {
|
|
31
|
+
const payload = JSON.stringify(data);
|
|
32
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
33
|
+
return `sha256:${hash}`;
|
|
34
|
+
}
|
|
35
|
+
determineStatus(data, daysSincePatch) {
|
|
36
|
+
if (data.criticalPatches > 0)
|
|
37
|
+
return "non_compliant";
|
|
38
|
+
if (daysSincePatch > 30)
|
|
39
|
+
return "non_compliant";
|
|
40
|
+
if (data.autoUpdateEnabled && daysSincePatch <= 14)
|
|
41
|
+
return "compliant";
|
|
42
|
+
if (daysSincePatch <= 30)
|
|
43
|
+
return "partial";
|
|
44
|
+
return "non_compliant";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { MFACollector } from "./MFACollector.js";
|
|
2
|
+
export { EncryptionCollector } from "./EncryptionCollector.js";
|
|
3
|
+
export { AccessControlCollector } from "./AccessControlCollector.js";
|
|
4
|
+
export { LoggingCollector } from "./LoggingCollector.js";
|
|
5
|
+
export { PatchManagementCollector } from "./PatchManagementCollector.js";
|
|
6
|
+
export { NetworkSecurityCollector } from "./NetworkSecurityCollector.js";
|
|
7
|
+
export { BackupCollector } from "./BackupCollector.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { MFACollector } from "./MFACollector.js";
|
|
2
|
+
export { EncryptionCollector } from "./EncryptionCollector.js";
|
|
3
|
+
export { AccessControlCollector } from "./AccessControlCollector.js";
|
|
4
|
+
export { LoggingCollector } from "./LoggingCollector.js";
|
|
5
|
+
export { PatchManagementCollector } from "./PatchManagementCollector.js";
|
|
6
|
+
export { NetworkSecurityCollector } from "./NetworkSecurityCollector.js";
|
|
7
|
+
export { BackupCollector } from "./BackupCollector.js";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { EvidenceCollectorEngine } from "./EvidenceCollector.js";
|
|
2
|
+
export type { EvidenceCollectionRequest, EvidenceCollectionResult } from "./EvidenceCollector.js";
|
|
3
|
+
export { MFACollector, EncryptionCollector, AccessControlCollector, LoggingCollector, PatchManagementCollector, NetworkSecurityCollector, BackupCollector, } from "./collectors/index.js";
|
|
4
|
+
export type { EvidenceCategory, ComplianceFramework, EvidenceItem, SystemAdapter, CollectorStatus, MFAEvidence, EncryptionEvidence, AccessControlEvidence, LoggingEvidence, PatchEvidence, NetworkSecurityEvidence, BackupEvidence, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { EvidenceCollectorEngine } from "./EvidenceCollector.js";
|
|
4
|
+
/** Mock SystemAdapter for testing */
|
|
5
|
+
const createMockAdapter = () => ({
|
|
6
|
+
queryMFA: async () => ({
|
|
7
|
+
enforced: true,
|
|
8
|
+
totalUsers: 100,
|
|
9
|
+
mfaEnabledUsers: 98,
|
|
10
|
+
methods: ["totp", "sms"],
|
|
11
|
+
lastEnforcedAt: "2025-01-15T00:00:00Z",
|
|
12
|
+
}),
|
|
13
|
+
queryEncryptionAtRest: async () => ({
|
|
14
|
+
enabled: true,
|
|
15
|
+
algorithm: "AES-256",
|
|
16
|
+
keyRotationDays: 90,
|
|
17
|
+
lastRotatedAt: "2025-06-01T00:00:00Z",
|
|
18
|
+
details: { provider: "aws-kms" },
|
|
19
|
+
}),
|
|
20
|
+
queryEncryptionInTransit: async () => ({
|
|
21
|
+
enabled: true,
|
|
22
|
+
algorithm: "TLS-1.3",
|
|
23
|
+
details: { minVersion: "TLSv1.3" },
|
|
24
|
+
}),
|
|
25
|
+
queryAccessControl: async () => ({
|
|
26
|
+
leastPrivilege: true,
|
|
27
|
+
totalRoles: 25,
|
|
28
|
+
excessiveRoles: 2,
|
|
29
|
+
lastAuditAt: "2025-05-01T00:00:00Z",
|
|
30
|
+
details: {},
|
|
31
|
+
}),
|
|
32
|
+
queryLogging: async () => ({
|
|
33
|
+
enabled: true,
|
|
34
|
+
logTypes: ["audit", "security", "application"],
|
|
35
|
+
retentionDays: 365,
|
|
36
|
+
alertingEnabled: true,
|
|
37
|
+
lastConfiguredAt: "2025-03-01T00:00:00Z",
|
|
38
|
+
}),
|
|
39
|
+
queryPatchManagement: async () => ({
|
|
40
|
+
lastPatchDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
41
|
+
pendingPatches: 3,
|
|
42
|
+
criticalPatches: 0,
|
|
43
|
+
autoUpdateEnabled: true,
|
|
44
|
+
details: {},
|
|
45
|
+
}),
|
|
46
|
+
queryNetworkSecurity: async () => ({
|
|
47
|
+
firewallEnabled: true,
|
|
48
|
+
segmentationEnabled: true,
|
|
49
|
+
totalRules: 42,
|
|
50
|
+
openPorts: 3,
|
|
51
|
+
lastAuditAt: "2025-06-10T00:00:00Z",
|
|
52
|
+
}),
|
|
53
|
+
queryBackup: async () => ({
|
|
54
|
+
configured: true,
|
|
55
|
+
frequency: "daily",
|
|
56
|
+
lastBackupAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
57
|
+
retentionDays: 90,
|
|
58
|
+
testedAt: "2025-06-01T00:00:00Z",
|
|
59
|
+
testPassed: true,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
describe("EvidenceCollectorEngine", () => {
|
|
63
|
+
it("should collect MFA evidence", async () => {
|
|
64
|
+
const engine = new EvidenceCollectorEngine(createMockAdapter());
|
|
65
|
+
const result = await engine.collectSingle({
|
|
66
|
+
category: "mfa",
|
|
67
|
+
framework: "SOC2",
|
|
68
|
+
controlId: "CC6.1",
|
|
69
|
+
});
|
|
70
|
+
assert.ok(result.id);
|
|
71
|
+
assert.equal(result.category, "mfa");
|
|
72
|
+
assert.equal(result.framework, "SOC2");
|
|
73
|
+
assert.equal(result.controlId, "CC6.1");
|
|
74
|
+
assert.ok(result.hash.startsWith("sha256:"));
|
|
75
|
+
assert.equal(result.status, "compliant");
|
|
76
|
+
});
|
|
77
|
+
it("should collect encryption evidence", async () => {
|
|
78
|
+
const engine = new EvidenceCollectorEngine(createMockAdapter());
|
|
79
|
+
const result = await engine.collectSingle({
|
|
80
|
+
category: "encryption",
|
|
81
|
+
framework: "ISO27001",
|
|
82
|
+
controlId: "A.10.1.1",
|
|
83
|
+
});
|
|
84
|
+
assert.equal(result.category, "encryption");
|
|
85
|
+
assert.equal(result.framework, "ISO27001");
|
|
86
|
+
assert.equal(result.status, "compliant");
|
|
87
|
+
});
|
|
88
|
+
it("should collect multiple evidence types in batch", async () => {
|
|
89
|
+
const engine = new EvidenceCollectorEngine(createMockAdapter());
|
|
90
|
+
const result = await engine.collect([
|
|
91
|
+
{ category: "mfa", framework: "SOC2", controlId: "CC6.1" },
|
|
92
|
+
{ category: "logging", framework: "SOC2", controlId: "CC7.1" },
|
|
93
|
+
{ category: "backup", framework: "SOC2", controlId: "CC7.2" },
|
|
94
|
+
]);
|
|
95
|
+
assert.equal(result.items.length, 3);
|
|
96
|
+
assert.equal(result.errors.length, 0);
|
|
97
|
+
assert.equal(result.status, "completed");
|
|
98
|
+
});
|
|
99
|
+
it("should provide compliance summary", async () => {
|
|
100
|
+
const engine = new EvidenceCollectorEngine(createMockAdapter());
|
|
101
|
+
await engine.collect([
|
|
102
|
+
{ category: "mfa", framework: "SOC2", controlId: "CC6.1" },
|
|
103
|
+
{ category: "encryption", framework: "SOC2", controlId: "CC6.2" },
|
|
104
|
+
{ category: "logging", framework: "SOC2", controlId: "CC7.1" },
|
|
105
|
+
]);
|
|
106
|
+
const summary = engine.getComplianceSummary("SOC2");
|
|
107
|
+
assert.equal(summary.total, 3);
|
|
108
|
+
assert.ok(summary.compliancePercentage >= 0);
|
|
109
|
+
});
|
|
110
|
+
it("should filter evidence by framework", async () => {
|
|
111
|
+
const engine = new EvidenceCollectorEngine(createMockAdapter());
|
|
112
|
+
await engine.collect([
|
|
113
|
+
{ category: "mfa", framework: "SOC2", controlId: "CC6.1" },
|
|
114
|
+
{ category: "mfa", framework: "ISO27001", controlId: "A.9.4.2" },
|
|
115
|
+
]);
|
|
116
|
+
const soc2 = engine.getEvidenceByFramework("SOC2");
|
|
117
|
+
const iso = engine.getEvidenceByFramework("ISO27001");
|
|
118
|
+
assert.equal(soc2.length, 1);
|
|
119
|
+
assert.equal(iso.length, 1);
|
|
120
|
+
});
|
|
121
|
+
it("should handle adapter errors gracefully", async () => {
|
|
122
|
+
const failingAdapter = {
|
|
123
|
+
...createMockAdapter(),
|
|
124
|
+
queryMFA: async () => {
|
|
125
|
+
throw new Error("IdP unavailable");
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
const engine = new EvidenceCollectorEngine(failingAdapter);
|
|
129
|
+
const result = await engine.collect([
|
|
130
|
+
{ category: "mfa", framework: "SOC2", controlId: "CC6.1" },
|
|
131
|
+
]);
|
|
132
|
+
assert.equal(result.items.length, 0);
|
|
133
|
+
assert.equal(result.errors.length, 1);
|
|
134
|
+
assert.ok(result.errors[0].includes("IdP unavailable"));
|
|
135
|
+
});
|
|
136
|
+
it("should clear evidence store", async () => {
|
|
137
|
+
const engine = new EvidenceCollectorEngine(createMockAdapter());
|
|
138
|
+
await engine.collect([
|
|
139
|
+
{ category: "mfa", framework: "SOC2", controlId: "CC6.1" },
|
|
140
|
+
]);
|
|
141
|
+
assert.equal(engine.getAllEvidence().length, 1);
|
|
142
|
+
engine.clearEvidence();
|
|
143
|
+
assert.equal(engine.getAllEvidence().length, 0);
|
|
144
|
+
});
|
|
145
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type EvidenceCategory = "mfa" | "encryption" | "access_control" | "logging" | "patch_management" | "network_security" | "backup";
|
|
2
|
+
export type ComplianceFramework = "SOC2" | "ISO27001" | "NIST_CSF";
|
|
3
|
+
export type CollectorStatus = "idle" | "collecting" | "completed" | "error";
|
|
4
|
+
export interface EvidenceItem {
|
|
5
|
+
id: string;
|
|
6
|
+
category: EvidenceCategory;
|
|
7
|
+
controlId: string;
|
|
8
|
+
framework: ComplianceFramework;
|
|
9
|
+
source: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
status: "compliant" | "non_compliant" | "partial" | "unknown";
|
|
14
|
+
}
|
|
15
|
+
export interface SystemAdapter {
|
|
16
|
+
/** Query MFA enforcement status */
|
|
17
|
+
queryMFA(): Promise<MFAEvidence>;
|
|
18
|
+
/** Query encryption at rest status */
|
|
19
|
+
queryEncryptionAtRest(): Promise<EncryptionEvidence>;
|
|
20
|
+
/** Query encryption in transit status */
|
|
21
|
+
queryEncryptionInTransit(): Promise<EncryptionEvidence>;
|
|
22
|
+
/** Query access control policies */
|
|
23
|
+
queryAccessControl(): Promise<AccessControlEvidence>;
|
|
24
|
+
/** Query logging configuration */
|
|
25
|
+
queryLogging(): Promise<LoggingEvidence>;
|
|
26
|
+
/** Query patch management status */
|
|
27
|
+
queryPatchManagement(): Promise<PatchEvidence>;
|
|
28
|
+
/** Query network security rules */
|
|
29
|
+
queryNetworkSecurity(): Promise<NetworkSecurityEvidence>;
|
|
30
|
+
/** Query backup configuration */
|
|
31
|
+
queryBackup(): Promise<BackupEvidence>;
|
|
32
|
+
}
|
|
33
|
+
export interface MFAEvidence {
|
|
34
|
+
enforced: boolean;
|
|
35
|
+
totalUsers: number;
|
|
36
|
+
mfaEnabledUsers: number;
|
|
37
|
+
methods: string[];
|
|
38
|
+
lastEnforcedAt?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface EncryptionEvidence {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
algorithm?: string;
|
|
43
|
+
keyRotationDays?: number;
|
|
44
|
+
lastRotatedAt?: string;
|
|
45
|
+
details: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
export interface AccessControlEvidence {
|
|
48
|
+
leastPrivilege: boolean;
|
|
49
|
+
totalRoles: number;
|
|
50
|
+
excessiveRoles: number;
|
|
51
|
+
lastAuditAt?: string;
|
|
52
|
+
details: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
export interface LoggingEvidence {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
logTypes: string[];
|
|
57
|
+
retentionDays: number;
|
|
58
|
+
alertingEnabled: boolean;
|
|
59
|
+
lastConfiguredAt?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface PatchEvidence {
|
|
62
|
+
lastPatchDate: string;
|
|
63
|
+
pendingPatches: number;
|
|
64
|
+
criticalPatches: number;
|
|
65
|
+
autoUpdateEnabled: boolean;
|
|
66
|
+
details: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
export interface NetworkSecurityEvidence {
|
|
69
|
+
firewallEnabled: boolean;
|
|
70
|
+
segmentationEnabled: boolean;
|
|
71
|
+
totalRules: number;
|
|
72
|
+
openPorts: number;
|
|
73
|
+
lastAuditAt?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface BackupEvidence {
|
|
76
|
+
configured: boolean;
|
|
77
|
+
frequency: string;
|
|
78
|
+
lastBackupAt?: string;
|
|
79
|
+
retentionDays: number;
|
|
80
|
+
testedAt?: string;
|
|
81
|
+
testPassed: boolean;
|
|
82
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grc-claw/evidence-collector",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Automated compliance evidence collection from real systems",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"test": "node --test dist/**/*.test.js 2>/dev/null || true"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/AAH20/GRC_Claw"
|
|
28
|
+
}
|
|
29
|
+
}
|