facult 2.5.2 → 2.7.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/README.md +7 -0
- package/package.json +1 -1
- package/src/audit/agent.ts +26 -24
- package/src/audit/fix.ts +875 -0
- package/src/audit/index.ts +51 -2
- package/src/audit/safe.ts +596 -0
- package/src/audit/static.ts +151 -34
- package/src/audit/status.ts +21 -0
- package/src/audit/suppressions.ts +266 -0
- package/src/audit/tui.ts +784 -174
- package/src/audit/update-index.ts +4 -17
- package/src/cli-ui.ts +375 -0
- package/src/consolidate.ts +151 -55
- package/src/global-docs.ts +38 -12
- package/src/index.ts +511 -239
- package/src/manage.ts +51 -51
- package/src/mcp-config.ts +132 -0
- package/src/remote.ts +387 -117
- package/src/trust.ts +119 -11
- package/src/util/git.ts +95 -0
package/src/audit/static.ts
CHANGED
|
@@ -2,14 +2,26 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
import {
|
|
5
|
+
import { loadManagedState } from "../manage";
|
|
6
|
+
import { isInlineMcpSecretValue } from "../mcp-config";
|
|
7
|
+
import {
|
|
8
|
+
facultContextRootDir,
|
|
9
|
+
facultRootDir,
|
|
10
|
+
facultStateDir,
|
|
11
|
+
readFacultConfig,
|
|
12
|
+
} from "../paths";
|
|
6
13
|
import type { ScanResult } from "../scan";
|
|
7
14
|
import { scan } from "../scan";
|
|
8
15
|
import {
|
|
9
16
|
extractCodexTomlMcpServerBlocks,
|
|
10
17
|
sanitizeCodexTomlMcpText,
|
|
11
18
|
} from "../util/codex-toml";
|
|
19
|
+
import { type GitPathExposure, getGitPathExposure } from "../util/git";
|
|
12
20
|
import { parseJsonLenient } from "../util/json";
|
|
21
|
+
import {
|
|
22
|
+
applyAuditSuppressionsToStaticReport,
|
|
23
|
+
loadAuditSuppressions,
|
|
24
|
+
} from "./suppressions";
|
|
13
25
|
import {
|
|
14
26
|
type AuditFinding,
|
|
15
27
|
type AuditItemResult,
|
|
@@ -440,14 +452,96 @@ function isSecretEnvKey(key: string): boolean {
|
|
|
440
452
|
return SECRET_ENV_KEY_RE.test(key);
|
|
441
453
|
}
|
|
442
454
|
|
|
455
|
+
function managedInlineSecretFinding(args: {
|
|
456
|
+
configPath: string;
|
|
457
|
+
envKey: string;
|
|
458
|
+
exposure: GitPathExposure;
|
|
459
|
+
serverName: string;
|
|
460
|
+
}): AuditFinding | null {
|
|
461
|
+
const location = `${args.configPath}:${args.serverName}:env:${args.envKey}`;
|
|
462
|
+
const evidence = `${args.envKey}=<redacted>`;
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
args.exposure.state === "outside-repo" ||
|
|
466
|
+
args.exposure.state === "ignored"
|
|
467
|
+
) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (args.exposure.state === "tracked") {
|
|
472
|
+
return {
|
|
473
|
+
severity: "critical",
|
|
474
|
+
ruleId: "mcp-env-inline-secret",
|
|
475
|
+
message:
|
|
476
|
+
"Managed MCP config includes an inline secret in a git-tracked file. Move the secret out of the repo or gitignore the rendered target.",
|
|
477
|
+
location,
|
|
478
|
+
evidence,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
severity: "high",
|
|
484
|
+
ruleId: "mcp-env-inline-secret",
|
|
485
|
+
message:
|
|
486
|
+
"Managed MCP config includes an inline secret in a repo-local file that is not gitignored. It may be committed accidentally.",
|
|
487
|
+
location,
|
|
488
|
+
evidence,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function canonicalInlineSecretFinding(args: {
|
|
493
|
+
configPath: string;
|
|
494
|
+
envKey: string;
|
|
495
|
+
exposure: GitPathExposure;
|
|
496
|
+
serverName: string;
|
|
497
|
+
}): AuditFinding {
|
|
498
|
+
const location = `${args.configPath}:${args.serverName}:env:${args.envKey}`;
|
|
499
|
+
const evidence = `${args.envKey}=<redacted>`;
|
|
500
|
+
|
|
501
|
+
if (args.exposure.state === "tracked") {
|
|
502
|
+
return {
|
|
503
|
+
severity: "critical",
|
|
504
|
+
ruleId: "mcp-env-inline-secret",
|
|
505
|
+
message:
|
|
506
|
+
"MCP server env includes an inline secret in a git-tracked file. Move it into local-only config or env indirection.",
|
|
507
|
+
location,
|
|
508
|
+
evidence,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (args.exposure.state === "untracked") {
|
|
513
|
+
return {
|
|
514
|
+
severity: "high",
|
|
515
|
+
ruleId: "mcp-env-inline-secret",
|
|
516
|
+
message:
|
|
517
|
+
"MCP server env includes an inline secret in a repo-local file that is not gitignored. It may be committed accidentally.",
|
|
518
|
+
location,
|
|
519
|
+
evidence,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
severity: "high",
|
|
525
|
+
ruleId: "mcp-env-inline-secret",
|
|
526
|
+
message:
|
|
527
|
+
"MCP server env includes what looks like a secret value (consider using indirection instead of inlining).",
|
|
528
|
+
location,
|
|
529
|
+
evidence,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
443
533
|
function structuredMcpChecks({
|
|
444
534
|
serverName,
|
|
445
535
|
configPath,
|
|
446
536
|
definition,
|
|
537
|
+
exposure,
|
|
538
|
+
isManagedOutput,
|
|
447
539
|
}: {
|
|
448
540
|
serverName: string;
|
|
449
541
|
configPath: string;
|
|
450
542
|
definition: unknown;
|
|
543
|
+
exposure: GitPathExposure;
|
|
544
|
+
isManagedOutput: boolean;
|
|
451
545
|
}): AuditFinding[] {
|
|
452
546
|
const findings: AuditFinding[] = [];
|
|
453
547
|
|
|
@@ -514,15 +608,23 @@ function structuredMcpChecks({
|
|
|
514
608
|
const secretKeys = Object.keys(env).filter((k) => isSecretEnvKey(k));
|
|
515
609
|
for (const k of secretKeys) {
|
|
516
610
|
const v = env[k];
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
611
|
+
if (isInlineMcpSecretValue(v)) {
|
|
612
|
+
const finding = isManagedOutput
|
|
613
|
+
? managedInlineSecretFinding({
|
|
614
|
+
configPath,
|
|
615
|
+
envKey: k,
|
|
616
|
+
exposure,
|
|
617
|
+
serverName,
|
|
618
|
+
})
|
|
619
|
+
: canonicalInlineSecretFinding({
|
|
620
|
+
configPath,
|
|
621
|
+
envKey: k,
|
|
622
|
+
exposure,
|
|
623
|
+
serverName,
|
|
624
|
+
});
|
|
625
|
+
if (finding) {
|
|
626
|
+
findings.push(finding);
|
|
627
|
+
}
|
|
526
628
|
}
|
|
527
629
|
}
|
|
528
630
|
}
|
|
@@ -713,6 +815,21 @@ export async function runStaticAudit(opts?: {
|
|
|
713
815
|
? { kind: "mcp", name: nameArg.slice("mcp:".length) }
|
|
714
816
|
: { kind: "skill", name: nameArg }
|
|
715
817
|
: null;
|
|
818
|
+
const managedRoots = uniqueSorted([
|
|
819
|
+
facultRootDir(home),
|
|
820
|
+
facultContextRootDir({ home, cwd: opts?.cwd }),
|
|
821
|
+
]);
|
|
822
|
+
const managedStates = await Promise.all(
|
|
823
|
+
managedRoots.map((rootDir) =>
|
|
824
|
+
loadManagedState(home, rootDir).catch(() => null)
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
const managedMcpConfigPaths = new Set(
|
|
828
|
+
managedStates
|
|
829
|
+
.flatMap((state) => Object.values(state?.tools ?? {}))
|
|
830
|
+
.map((entry) => entry.mcpConfig)
|
|
831
|
+
.filter((path): path is string => typeof path === "string")
|
|
832
|
+
);
|
|
716
833
|
|
|
717
834
|
const results: AuditItemResult[] = [];
|
|
718
835
|
|
|
@@ -860,6 +977,8 @@ export async function runStaticAudit(opts?: {
|
|
|
860
977
|
for (const cfg of Array.from(uniqMcpConfigs.values()).sort((a, b) =>
|
|
861
978
|
a.path.localeCompare(b.path)
|
|
862
979
|
)) {
|
|
980
|
+
const exposure = await getGitPathExposure(cfg.path);
|
|
981
|
+
const isManagedOutput = managedMcpConfigPaths.has(cfg.path);
|
|
863
982
|
const isToml = cfg.format === "toml" || cfg.path.endsWith(".toml");
|
|
864
983
|
|
|
865
984
|
if (isToml) {
|
|
@@ -981,6 +1100,8 @@ export async function runStaticAudit(opts?: {
|
|
|
981
1100
|
serverName,
|
|
982
1101
|
configPath: cfg.path,
|
|
983
1102
|
definition,
|
|
1103
|
+
exposure,
|
|
1104
|
+
isManagedOutput,
|
|
984
1105
|
});
|
|
985
1106
|
|
|
986
1107
|
const findings = [...ruleFindings, ...structured].sort((a, b) => {
|
|
@@ -1006,27 +1127,7 @@ export async function runStaticAudit(opts?: {
|
|
|
1006
1127
|
}
|
|
1007
1128
|
|
|
1008
1129
|
const minSeverity = opts?.minSeverity ?? undefined;
|
|
1009
|
-
|
|
1010
|
-
low: 0,
|
|
1011
|
-
medium: 0,
|
|
1012
|
-
high: 0,
|
|
1013
|
-
critical: 0,
|
|
1014
|
-
};
|
|
1015
|
-
let totalFindings = 0;
|
|
1016
|
-
let flaggedItems = 0;
|
|
1017
|
-
|
|
1018
|
-
for (const r of results) {
|
|
1019
|
-
const all = r.findings;
|
|
1020
|
-
totalFindings += all.length;
|
|
1021
|
-
if (!r.passed) {
|
|
1022
|
-
flaggedItems += 1;
|
|
1023
|
-
}
|
|
1024
|
-
for (const f of all) {
|
|
1025
|
-
bySeverity[f.severity] += 1;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const report: StaticAuditReport = {
|
|
1130
|
+
let report: StaticAuditReport = {
|
|
1030
1131
|
timestamp: new Date().toISOString(),
|
|
1031
1132
|
mode: "static",
|
|
1032
1133
|
minSeverity,
|
|
@@ -1034,12 +1135,28 @@ export async function runStaticAudit(opts?: {
|
|
|
1034
1135
|
results,
|
|
1035
1136
|
summary: {
|
|
1036
1137
|
totalItems: results.length,
|
|
1037
|
-
totalFindings
|
|
1038
|
-
|
|
1039
|
-
|
|
1138
|
+
totalFindings: results.reduce(
|
|
1139
|
+
(sum, result) => sum + result.findings.length,
|
|
1140
|
+
0
|
|
1141
|
+
),
|
|
1142
|
+
bySeverity: results.reduce<Record<Severity, number>>(
|
|
1143
|
+
(acc, result) => {
|
|
1144
|
+
for (const finding of result.findings) {
|
|
1145
|
+
acc[finding.severity] += 1;
|
|
1146
|
+
}
|
|
1147
|
+
return acc;
|
|
1148
|
+
},
|
|
1149
|
+
{ low: 0, medium: 0, high: 0, critical: 0 }
|
|
1150
|
+
),
|
|
1151
|
+
flaggedItems: results.filter((result) => !result.passed).length,
|
|
1040
1152
|
},
|
|
1041
1153
|
};
|
|
1042
1154
|
|
|
1155
|
+
report = applyAuditSuppressionsToStaticReport(
|
|
1156
|
+
report,
|
|
1157
|
+
await loadAuditSuppressions(home)
|
|
1158
|
+
);
|
|
1159
|
+
|
|
1043
1160
|
const auditDir = join(facultStateDir(home), "audit");
|
|
1044
1161
|
await ensureDir(auditDir);
|
|
1045
1162
|
await Bun.write(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AuditFinding } from "./types";
|
|
2
|
+
import { SEVERITY_ORDER } from "./types";
|
|
3
|
+
|
|
4
|
+
export type StoredAuditStatus = "pending" | "passed" | "flagged";
|
|
5
|
+
|
|
6
|
+
export function computeStoredAuditStatus(
|
|
7
|
+
findings: Pick<AuditFinding, "severity" | "ruleId">[]
|
|
8
|
+
): StoredAuditStatus {
|
|
9
|
+
if (findings.some((finding) => finding.ruleId === "agent-error")) {
|
|
10
|
+
return "pending";
|
|
11
|
+
}
|
|
12
|
+
const worst = findings.reduce(
|
|
13
|
+
(max, finding) => Math.max(max, SEVERITY_ORDER[finding.severity]),
|
|
14
|
+
-1
|
|
15
|
+
);
|
|
16
|
+
return worst >= SEVERITY_ORDER.high ? "flagged" : "passed";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isStoredAuditStatusPassed(status: StoredAuditStatus): boolean {
|
|
20
|
+
return status === "passed";
|
|
21
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { facultStateDir } from "../paths";
|
|
5
|
+
import type { AgentAuditReport } from "./agent";
|
|
6
|
+
import { computeStoredAuditStatus, isStoredAuditStatusPassed } from "./status";
|
|
7
|
+
import type {
|
|
8
|
+
AuditFinding,
|
|
9
|
+
AuditItemResult,
|
|
10
|
+
Severity,
|
|
11
|
+
StaticAuditReport,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
const RULE_ID_PREFIX_RE = /^(static|agent):/;
|
|
15
|
+
|
|
16
|
+
export interface AuditSuppressionEntry {
|
|
17
|
+
key: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
type: AuditItemResult["type"];
|
|
20
|
+
item: string;
|
|
21
|
+
path: string;
|
|
22
|
+
finding: {
|
|
23
|
+
severity: Severity;
|
|
24
|
+
ruleId: string;
|
|
25
|
+
message: string;
|
|
26
|
+
location?: string;
|
|
27
|
+
};
|
|
28
|
+
note?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AuditSuppressionStore {
|
|
32
|
+
version: 1;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
entries: AuditSuppressionEntry[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeRuleId(ruleId: string): string {
|
|
38
|
+
return ruleId.replace(RULE_ID_PREFIX_RE, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizedFindingSignature(args: {
|
|
42
|
+
type: AuditItemResult["type"];
|
|
43
|
+
item: string;
|
|
44
|
+
path: string;
|
|
45
|
+
finding: Pick<AuditFinding, "severity" | "ruleId" | "message" | "location">;
|
|
46
|
+
}): string {
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
type: args.type,
|
|
49
|
+
item: args.item,
|
|
50
|
+
path: args.path,
|
|
51
|
+
severity: args.finding.severity,
|
|
52
|
+
ruleId: normalizeRuleId(args.finding.ruleId),
|
|
53
|
+
message: args.finding.message,
|
|
54
|
+
location: args.finding.location ?? "",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function suppressionsPath(homeDir: string): string {
|
|
59
|
+
return join(facultStateDir(homeDir), "audit", "suppressions.json");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createAuditSuppressionEntry(args: {
|
|
63
|
+
result: AuditItemResult;
|
|
64
|
+
finding: AuditFinding;
|
|
65
|
+
createdAt?: string;
|
|
66
|
+
note?: string;
|
|
67
|
+
}): AuditSuppressionEntry {
|
|
68
|
+
const createdAt = args.createdAt ?? new Date().toISOString();
|
|
69
|
+
return {
|
|
70
|
+
key: normalizedFindingSignature({
|
|
71
|
+
type: args.result.type,
|
|
72
|
+
item: args.result.item,
|
|
73
|
+
path: args.result.path,
|
|
74
|
+
finding: args.finding,
|
|
75
|
+
}),
|
|
76
|
+
createdAt,
|
|
77
|
+
type: args.result.type,
|
|
78
|
+
item: args.result.item,
|
|
79
|
+
path: args.result.path,
|
|
80
|
+
finding: {
|
|
81
|
+
severity: args.finding.severity,
|
|
82
|
+
ruleId: args.finding.ruleId,
|
|
83
|
+
message: args.finding.message,
|
|
84
|
+
location: args.finding.location,
|
|
85
|
+
},
|
|
86
|
+
note: args.note?.trim() ? args.note.trim() : undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadAuditSuppressionStore(
|
|
91
|
+
homeDir: string
|
|
92
|
+
): Promise<AuditSuppressionStore> {
|
|
93
|
+
const path = suppressionsPath(homeDir);
|
|
94
|
+
const file = Bun.file(path);
|
|
95
|
+
if (!(await file.exists())) {
|
|
96
|
+
return {
|
|
97
|
+
version: 1,
|
|
98
|
+
updatedAt: new Date(0).toISOString(),
|
|
99
|
+
entries: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const parsed = (await file.json()) as Partial<AuditSuppressionStore>;
|
|
104
|
+
return {
|
|
105
|
+
version: 1,
|
|
106
|
+
updatedAt:
|
|
107
|
+
typeof parsed.updatedAt === "string"
|
|
108
|
+
? parsed.updatedAt
|
|
109
|
+
: new Date(0).toISOString(),
|
|
110
|
+
entries: Array.isArray(parsed.entries)
|
|
111
|
+
? parsed.entries.filter(
|
|
112
|
+
(entry): entry is AuditSuppressionEntry =>
|
|
113
|
+
!!entry &&
|
|
114
|
+
typeof entry === "object" &&
|
|
115
|
+
typeof (entry as AuditSuppressionEntry).key === "string"
|
|
116
|
+
)
|
|
117
|
+
: [],
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return {
|
|
121
|
+
version: 1,
|
|
122
|
+
updatedAt: new Date(0).toISOString(),
|
|
123
|
+
entries: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function writeAuditSuppressionStore(
|
|
129
|
+
homeDir: string,
|
|
130
|
+
store: AuditSuppressionStore
|
|
131
|
+
) {
|
|
132
|
+
const path = suppressionsPath(homeDir);
|
|
133
|
+
await mkdir(join(facultStateDir(homeDir), "audit"), { recursive: true });
|
|
134
|
+
await Bun.write(path, `${JSON.stringify(store, null, 2)}\n`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function loadAuditSuppressions(
|
|
138
|
+
homeDir = homedir()
|
|
139
|
+
): Promise<AuditSuppressionEntry[]> {
|
|
140
|
+
return (await loadAuditSuppressionStore(homeDir)).entries;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function recordAuditSuppressions(args: {
|
|
144
|
+
selected: { result: AuditItemResult; finding: AuditFinding }[];
|
|
145
|
+
homeDir?: string;
|
|
146
|
+
note?: string;
|
|
147
|
+
}): Promise<{ added: number; total: number }> {
|
|
148
|
+
const homeDir = args.homeDir ?? homedir();
|
|
149
|
+
const store = await loadAuditSuppressionStore(homeDir);
|
|
150
|
+
const next = new Map(store.entries.map((entry) => [entry.key, entry]));
|
|
151
|
+
const createdAt = new Date().toISOString();
|
|
152
|
+
|
|
153
|
+
for (const selection of args.selected) {
|
|
154
|
+
const entry = createAuditSuppressionEntry({
|
|
155
|
+
result: selection.result,
|
|
156
|
+
finding: selection.finding,
|
|
157
|
+
createdAt,
|
|
158
|
+
note: args.note,
|
|
159
|
+
});
|
|
160
|
+
next.set(entry.key, entry);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const entries = [...next.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
164
|
+
await writeAuditSuppressionStore(homeDir, {
|
|
165
|
+
version: 1,
|
|
166
|
+
updatedAt: createdAt,
|
|
167
|
+
entries,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
added: Math.max(0, entries.length - store.entries.length),
|
|
172
|
+
total: entries.length,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function applyAuditSuppressionsToResults(args: {
|
|
177
|
+
results: AuditItemResult[];
|
|
178
|
+
suppressions: AuditSuppressionEntry[];
|
|
179
|
+
}): AuditItemResult[] {
|
|
180
|
+
if (args.suppressions.length === 0) {
|
|
181
|
+
return args.results;
|
|
182
|
+
}
|
|
183
|
+
const suppressedKeys = new Set(args.suppressions.map((entry) => entry.key));
|
|
184
|
+
return args.results.map((result) => {
|
|
185
|
+
const findings = result.findings.filter(
|
|
186
|
+
(finding) =>
|
|
187
|
+
!suppressedKeys.has(
|
|
188
|
+
normalizedFindingSignature({
|
|
189
|
+
type: result.type,
|
|
190
|
+
item: result.item,
|
|
191
|
+
path: result.path,
|
|
192
|
+
finding,
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
const status = computeStoredAuditStatus(findings);
|
|
197
|
+
return {
|
|
198
|
+
...result,
|
|
199
|
+
passed: isStoredAuditStatusPassed(status),
|
|
200
|
+
findings,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function summarizeResults(results: AuditItemResult[]): {
|
|
206
|
+
totalItems: number;
|
|
207
|
+
totalFindings: number;
|
|
208
|
+
bySeverity: Record<Severity, number>;
|
|
209
|
+
flaggedItems: number;
|
|
210
|
+
} {
|
|
211
|
+
const bySeverity: Record<Severity, number> = {
|
|
212
|
+
low: 0,
|
|
213
|
+
medium: 0,
|
|
214
|
+
high: 0,
|
|
215
|
+
critical: 0,
|
|
216
|
+
};
|
|
217
|
+
let totalFindings = 0;
|
|
218
|
+
let flaggedItems = 0;
|
|
219
|
+
|
|
220
|
+
for (const result of results) {
|
|
221
|
+
totalFindings += result.findings.length;
|
|
222
|
+
if (!result.passed && result.findings.length > 0) {
|
|
223
|
+
flaggedItems += 1;
|
|
224
|
+
}
|
|
225
|
+
for (const finding of result.findings) {
|
|
226
|
+
bySeverity[finding.severity] += 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
totalItems: results.length,
|
|
232
|
+
totalFindings,
|
|
233
|
+
bySeverity,
|
|
234
|
+
flaggedItems,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function applyAuditSuppressionsToStaticReport(
|
|
239
|
+
report: StaticAuditReport,
|
|
240
|
+
suppressions: AuditSuppressionEntry[]
|
|
241
|
+
): StaticAuditReport {
|
|
242
|
+
const results = applyAuditSuppressionsToResults({
|
|
243
|
+
results: report.results,
|
|
244
|
+
suppressions,
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
...report,
|
|
248
|
+
results,
|
|
249
|
+
summary: summarizeResults(results),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function applyAuditSuppressionsToAgentReport(
|
|
254
|
+
report: AgentAuditReport,
|
|
255
|
+
suppressions: AuditSuppressionEntry[]
|
|
256
|
+
): AgentAuditReport {
|
|
257
|
+
const results = applyAuditSuppressionsToResults({
|
|
258
|
+
results: report.results,
|
|
259
|
+
suppressions,
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
...report,
|
|
263
|
+
results,
|
|
264
|
+
summary: summarizeResults(results),
|
|
265
|
+
};
|
|
266
|
+
}
|