codex-plugin-doctor 1.26.0 → 1.28.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 +114 -84
- package/dist/core/doctor-config-store.d.ts +12 -0
- package/dist/core/doctor-config-store.js +61 -0
- package/dist/core/doctor-config.d.ts +4 -1
- package/dist/core/doctor-config.js +78 -5
- package/dist/core/npm-package-doctor.js +8 -0
- package/dist/core/output-contract.js +18 -0
- package/dist/core/suppression-management.d.ts +34 -0
- package/dist/core/suppression-management.js +170 -0
- package/dist/core/suppression-record.d.ts +26 -0
- package/dist/core/suppression-record.js +62 -0
- package/dist/domain/types.d.ts +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/reporting/render-json-report.js +7 -1
- package/dist/reporting/render-markdown-report.js +35 -15
- package/dist/reporting/render-sarif-report.js +9 -1
- package/dist/reporting/render-suppression-management.d.ts +7 -0
- package/dist/reporting/render-suppression-management.js +117 -0
- package/dist/reporting/render-text-report.js +14 -1
- package/dist/rules/rule-catalog.d.ts +1 -1
- package/dist/rules/rule-catalog.js +18 -0
- package/dist/run-cli.d.ts +3 -0
- package/dist/run-cli.js +411 -1
- package/package.json +64 -64
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { classifySuppressionRecord, isValidSuppressionFingerprint, validateSuppressionRecord } from "./suppression-record.js";
|
|
2
|
+
export class SuppressionManagementError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
field;
|
|
5
|
+
index;
|
|
6
|
+
indexes;
|
|
7
|
+
constructor(message, details) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "SuppressionManagementError";
|
|
10
|
+
this.code = details.code;
|
|
11
|
+
this.field = details.field;
|
|
12
|
+
this.index = details.index;
|
|
13
|
+
this.indexes = details.indexes;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function createSuppressionManagementError(message, details) {
|
|
17
|
+
return new SuppressionManagementError(message, details);
|
|
18
|
+
}
|
|
19
|
+
function isPlainObject(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function readSuppressions(config) {
|
|
23
|
+
if (!("suppressions" in config)) {
|
|
24
|
+
return {
|
|
25
|
+
suppressions: [],
|
|
26
|
+
hasSuppressions: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const { suppressions } = config;
|
|
30
|
+
if (!Array.isArray(suppressions)) {
|
|
31
|
+
throw createSuppressionManagementError("Doctor config suppressions must be an array when present.", {
|
|
32
|
+
code: "suppression_non_array"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
suppressions,
|
|
37
|
+
hasSuppressions: true
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function copyConfigWithSuppressions(config, suppressions) {
|
|
41
|
+
return {
|
|
42
|
+
...config,
|
|
43
|
+
suppressions
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function buildManagedInvalidSuppression(index, invalidField) {
|
|
47
|
+
return {
|
|
48
|
+
index,
|
|
49
|
+
status: "invalid",
|
|
50
|
+
invalidField
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function buildRemovedSuppressionSummary(suppression) {
|
|
54
|
+
const classified = classifySuppressionRecord(suppression, new Date(0));
|
|
55
|
+
if (classified.status === "invalid") {
|
|
56
|
+
return {
|
|
57
|
+
invalidField: classified.field
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
fingerprint: classified.fingerprint,
|
|
62
|
+
reason: classified.reason,
|
|
63
|
+
expiresAt: classified.expiresAt
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function findDuplicateFingerprintIndex(suppressions, fingerprint) {
|
|
67
|
+
return suppressions.findIndex((suppression) => isPlainObject(suppression) &&
|
|
68
|
+
typeof suppression.fingerprint === "string" &&
|
|
69
|
+
suppression.fingerprint === fingerprint);
|
|
70
|
+
}
|
|
71
|
+
function validateManagedFingerprint(fingerprint) {
|
|
72
|
+
if (!isValidSuppressionFingerprint(fingerprint)) {
|
|
73
|
+
throw createSuppressionManagementError("Suppression fingerprint is invalid.", {
|
|
74
|
+
code: "suppression_invalid_fingerprint"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return fingerprint;
|
|
78
|
+
}
|
|
79
|
+
function findFingerprintMatches(suppressions, fingerprint) {
|
|
80
|
+
return suppressions.flatMap((suppression, index) => isPlainObject(suppression) &&
|
|
81
|
+
typeof suppression.fingerprint === "string" &&
|
|
82
|
+
suppression.fingerprint === fingerprint
|
|
83
|
+
? [index]
|
|
84
|
+
: []);
|
|
85
|
+
}
|
|
86
|
+
export function listSuppressions(config, now = new Date()) {
|
|
87
|
+
const { suppressions, hasSuppressions } = readSuppressions(config);
|
|
88
|
+
if (!hasSuppressions) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return suppressions.map((suppression, index) => {
|
|
92
|
+
const classified = classifySuppressionRecord(suppression, now);
|
|
93
|
+
if (classified.status === "invalid") {
|
|
94
|
+
return buildManagedInvalidSuppression(index, classified.field);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
index,
|
|
98
|
+
status: classified.status,
|
|
99
|
+
fingerprint: classified.fingerprint,
|
|
100
|
+
reason: classified.reason,
|
|
101
|
+
expiresAt: classified.expiresAt
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function addSuppression(config, record) {
|
|
106
|
+
const { suppressions } = readSuppressions(config);
|
|
107
|
+
const validation = validateSuppressionRecord(record);
|
|
108
|
+
if (!validation.valid) {
|
|
109
|
+
throw createSuppressionManagementError(`Suppression record is invalid: ${validation.field}.`, {
|
|
110
|
+
code: "suppression_invalid_record",
|
|
111
|
+
field: validation.field
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const duplicateIndex = findDuplicateFingerprintIndex(suppressions, validation.fingerprint);
|
|
115
|
+
if (duplicateIndex >= 0) {
|
|
116
|
+
throw createSuppressionManagementError(`Suppression fingerprint already exists at index ${duplicateIndex}.`, {
|
|
117
|
+
code: "suppression_duplicate_fingerprint",
|
|
118
|
+
index: duplicateIndex
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const suppression = {
|
|
122
|
+
fingerprint: validation.fingerprint,
|
|
123
|
+
reason: validation.reason,
|
|
124
|
+
expiresAt: validation.expiresAt
|
|
125
|
+
};
|
|
126
|
+
const nextSuppressions = [...suppressions, suppression];
|
|
127
|
+
return {
|
|
128
|
+
config: copyConfigWithSuppressions(config, nextSuppressions),
|
|
129
|
+
index: suppressions.length,
|
|
130
|
+
suppression
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function removeSuppressionByIndex(config, index) {
|
|
134
|
+
const { suppressions } = readSuppressions(config);
|
|
135
|
+
if (!Number.isInteger(index) ||
|
|
136
|
+
index < 0 ||
|
|
137
|
+
index >= suppressions.length) {
|
|
138
|
+
throw createSuppressionManagementError("Suppression index must be an integer within range.", {
|
|
139
|
+
code: "suppression_invalid_index",
|
|
140
|
+
index
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const suppression = suppressions[index];
|
|
144
|
+
const nextSuppressions = [
|
|
145
|
+
...suppressions.slice(0, index),
|
|
146
|
+
...suppressions.slice(index + 1)
|
|
147
|
+
];
|
|
148
|
+
return {
|
|
149
|
+
config: copyConfigWithSuppressions(config, nextSuppressions),
|
|
150
|
+
index,
|
|
151
|
+
suppression: buildRemovedSuppressionSummary(suppression)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function removeSuppressionByFingerprint(config, fingerprint) {
|
|
155
|
+
const { suppressions } = readSuppressions(config);
|
|
156
|
+
const validFingerprint = validateManagedFingerprint(fingerprint);
|
|
157
|
+
const matches = findFingerprintMatches(suppressions, validFingerprint);
|
|
158
|
+
if (matches.length === 0) {
|
|
159
|
+
throw createSuppressionManagementError("Suppression fingerprint not found.", {
|
|
160
|
+
code: "suppression_fingerprint_not_found"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (matches.length > 1) {
|
|
164
|
+
throw createSuppressionManagementError(`Suppression fingerprint matches multiple suppressions at indexes: ${matches.join(", ")}.`, {
|
|
165
|
+
code: "suppression_fingerprint_ambiguous",
|
|
166
|
+
indexes: matches
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return removeSuppressionByIndex(config, matches[0]);
|
|
170
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SuppressionRecordField = "record" | "fingerprint" | "reason" | "expiresAt";
|
|
2
|
+
export interface ValidSuppressionRecord {
|
|
3
|
+
valid: true;
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
expiresAt: string;
|
|
7
|
+
}
|
|
8
|
+
export interface InvalidSuppressionRecord {
|
|
9
|
+
valid: false;
|
|
10
|
+
field: SuppressionRecordField;
|
|
11
|
+
}
|
|
12
|
+
type ActiveSuppressionRecord = ValidSuppressionRecord & {
|
|
13
|
+
status: "active";
|
|
14
|
+
};
|
|
15
|
+
type ExpiredSuppressionRecord = ValidSuppressionRecord & {
|
|
16
|
+
status: "expired";
|
|
17
|
+
};
|
|
18
|
+
type InvalidClassifiedSuppressionRecord = InvalidSuppressionRecord & {
|
|
19
|
+
status: "invalid";
|
|
20
|
+
};
|
|
21
|
+
export type ClassifiedSuppressionRecord = ActiveSuppressionRecord | ExpiredSuppressionRecord | InvalidClassifiedSuppressionRecord;
|
|
22
|
+
export declare function isValidSuppressionFingerprint(value: unknown): value is string;
|
|
23
|
+
export declare function validateSuppressionRecord(value: unknown): ValidSuppressionRecord | InvalidSuppressionRecord;
|
|
24
|
+
export declare function isSuppressionExpired(expiresAt: string, now: Date): boolean;
|
|
25
|
+
export declare function classifySuppressionRecord(value: unknown, now: Date): ClassifiedSuppressionRecord;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function isRealDate(value) {
|
|
5
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
6
|
+
if (!match) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const year = Number(match[1]);
|
|
10
|
+
const month = Number(match[2]);
|
|
11
|
+
const day = Number(match[3]);
|
|
12
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
13
|
+
return (date.getUTCFullYear() === year &&
|
|
14
|
+
date.getUTCMonth() === month - 1 &&
|
|
15
|
+
date.getUTCDate() === day);
|
|
16
|
+
}
|
|
17
|
+
export function isValidSuppressionFingerprint(value) {
|
|
18
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
19
|
+
}
|
|
20
|
+
export function validateSuppressionRecord(value) {
|
|
21
|
+
if (!isPlainObject(value)) {
|
|
22
|
+
return { valid: false, field: "record" };
|
|
23
|
+
}
|
|
24
|
+
if (!isValidSuppressionFingerprint(value.fingerprint)) {
|
|
25
|
+
return { valid: false, field: "fingerprint" };
|
|
26
|
+
}
|
|
27
|
+
if (typeof value.reason !== "string" || value.reason.trim().length === 0) {
|
|
28
|
+
return { valid: false, field: "reason" };
|
|
29
|
+
}
|
|
30
|
+
if (typeof value.expiresAt !== "string" || !isRealDate(value.expiresAt)) {
|
|
31
|
+
return { valid: false, field: "expiresAt" };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
valid: true,
|
|
35
|
+
fingerprint: value.fingerprint,
|
|
36
|
+
reason: value.reason.trim(),
|
|
37
|
+
expiresAt: value.expiresAt
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function isSuppressionExpired(expiresAt, now) {
|
|
41
|
+
const expirationBoundary = Date.parse(`${expiresAt}T00:00:00.000Z`) + 86_400_000;
|
|
42
|
+
return now.getTime() >= expirationBoundary;
|
|
43
|
+
}
|
|
44
|
+
export function classifySuppressionRecord(value, now) {
|
|
45
|
+
const validation = validateSuppressionRecord(value);
|
|
46
|
+
if (!validation.valid) {
|
|
47
|
+
return {
|
|
48
|
+
status: "invalid",
|
|
49
|
+
...validation
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (isSuppressionExpired(validation.expiresAt, now)) {
|
|
53
|
+
return {
|
|
54
|
+
status: "expired",
|
|
55
|
+
...validation
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
status: "active",
|
|
60
|
+
...validation
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -10,11 +10,25 @@ export interface Finding {
|
|
|
10
10
|
evidence?: FindingEvidence;
|
|
11
11
|
fingerprint?: string;
|
|
12
12
|
}
|
|
13
|
+
export interface FindingSuppression {
|
|
14
|
+
reason: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SuppressedFinding extends Finding {
|
|
18
|
+
suppression: FindingSuppression;
|
|
19
|
+
}
|
|
20
|
+
export interface SuppressionSummary {
|
|
21
|
+
applied: number;
|
|
22
|
+
expired: number;
|
|
23
|
+
invalid: number;
|
|
24
|
+
}
|
|
13
25
|
export interface CheckResult {
|
|
14
26
|
targetPath: string;
|
|
15
27
|
status: "pass" | "warn" | "fail";
|
|
16
28
|
exitCode: 0 | 1;
|
|
17
29
|
findings: Finding[];
|
|
30
|
+
suppressedFindings?: SuppressedFinding[];
|
|
31
|
+
suppressionSummary?: SuppressionSummary;
|
|
18
32
|
runtimeScorecard?: RuntimeScorecard;
|
|
19
33
|
}
|
|
20
34
|
export interface CheckOptions {
|
|
@@ -50,6 +64,8 @@ export interface JsonReport {
|
|
|
50
64
|
generatedAt: string;
|
|
51
65
|
summary: JsonReportSummary;
|
|
52
66
|
findings: Finding[];
|
|
67
|
+
suppressedFindings?: SuppressedFinding[];
|
|
68
|
+
suppressionSummary?: SuppressionSummary;
|
|
53
69
|
}
|
|
54
70
|
export type RuntimeCapabilityStatus = "pass" | "fail" | "warn" | "skipped" | "unsupported";
|
|
55
71
|
export interface RuntimeScorecard {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { CheckOptions, CheckResult } from "./domain/types.js";
|
|
2
|
+
export { applyDoctorConfig, loadDoctorConfig, type DoctorConfig } from "./core/doctor-config.js";
|
|
3
|
+
export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig, type LoadedDoctorConfig, type RawDoctorConfig } from "./core/doctor-config-store.js";
|
|
4
|
+
export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord, type ClassifiedSuppressionRecord, type InvalidSuppressionRecord, type SuppressionRecordField, type ValidSuppressionRecord } from "./core/suppression-record.js";
|
|
5
|
+
export { addSuppression, listSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError, type ManagedSuppressionRecord, type SuppressionManagementErrorCode, type SuppressionMutationResult } from "./core/suppression-management.js";
|
|
2
6
|
export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
|
|
3
7
|
export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard, type SecurityAudit } from "./security/security-audit.js";
|
|
4
8
|
export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrustScoreOptions, type TrustScoreReport } from "./security/trust-score.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { validatePlugin } from "./core/validate-plugin.js";
|
|
2
|
+
export { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
3
|
+
export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig } from "./core/doctor-config-store.js";
|
|
4
|
+
export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord } from "./core/suppression-record.js";
|
|
5
|
+
export { addSuppression, listSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError } from "./core/suppression-management.js";
|
|
2
6
|
export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
|
|
3
7
|
export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
|
|
4
8
|
export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
|
|
@@ -18,7 +18,13 @@ export function buildJsonReport(result, options) {
|
|
|
18
18
|
total: result.findings.length
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
-
findings: result.findings
|
|
21
|
+
findings: result.findings,
|
|
22
|
+
...(result.suppressedFindings
|
|
23
|
+
? { suppressedFindings: result.suppressedFindings }
|
|
24
|
+
: {}),
|
|
25
|
+
...(result.suppressionSummary
|
|
26
|
+
? { suppressionSummary: result.suppressionSummary }
|
|
27
|
+
: {})
|
|
22
28
|
};
|
|
23
29
|
}
|
|
24
30
|
export function renderJsonReport(result, options) {
|
|
@@ -16,7 +16,10 @@ export function buildMarkdownReport(result, options) {
|
|
|
16
16
|
`| Warn Findings | ${warnCount} |`,
|
|
17
17
|
`| Total Findings | ${result.findings.length} |`
|
|
18
18
|
];
|
|
19
|
-
if (result.
|
|
19
|
+
if (result.suppressionSummary) {
|
|
20
|
+
lines.push(`| Suppressions Applied | ${result.suppressionSummary.applied} |`, `| Suppressions Expired | ${result.suppressionSummary.expired} |`, `| Suppressions Invalid | ${result.suppressionSummary.invalid} |`);
|
|
21
|
+
}
|
|
22
|
+
if (result.findings.length === 0 && !result.suppressedFindings?.length) {
|
|
20
23
|
lines.push("", "No findings.");
|
|
21
24
|
return lines.join("\n");
|
|
22
25
|
}
|
|
@@ -33,22 +36,39 @@ export function buildMarkdownReport(result, options) {
|
|
|
33
36
|
lines.push(`| prompts/list | ${result.runtimeScorecard.promptsList.toUpperCase()} |`);
|
|
34
37
|
lines.push(`| prompts/get | ${result.runtimeScorecard.promptGet.toUpperCase()} |`);
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
if (result.findings.length > 0) {
|
|
40
|
+
lines.push("", "## Findings", "");
|
|
41
|
+
for (const finding of result.findings) {
|
|
42
|
+
lines.push(`### ${finding.severity.toUpperCase()} \`${finding.id}\``);
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(`- Message: ${finding.message}`);
|
|
45
|
+
lines.push(`- Impact: ${finding.impact}`);
|
|
46
|
+
lines.push(`- Suggested fix: ${finding.suggestedFix}`);
|
|
47
|
+
const fingerprint = formatFindingFingerprintLine(finding);
|
|
48
|
+
if (fingerprint) {
|
|
49
|
+
lines.push(`- Fingerprint: \`${fingerprint}\``);
|
|
50
|
+
}
|
|
51
|
+
const evidence = formatFindingEvidenceLine(finding);
|
|
52
|
+
if (evidence) {
|
|
53
|
+
lines.push(`- Evidence: ${evidence}`);
|
|
54
|
+
}
|
|
55
|
+
lines.push("");
|
|
46
56
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
lines.push("", "No active findings.");
|
|
60
|
+
}
|
|
61
|
+
if (result.suppressedFindings?.length) {
|
|
62
|
+
lines.push("", "## Suppressed Findings", "");
|
|
63
|
+
for (const finding of result.suppressedFindings) {
|
|
64
|
+
lines.push(`### ${finding.severity.toUpperCase()} \`${finding.id}\``);
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(`- Message: ${finding.message}`);
|
|
67
|
+
lines.push(`- Fingerprint: \`${finding.fingerprint ?? "unavailable"}\``);
|
|
68
|
+
lines.push(`- Reason: ${finding.suppression.reason}`);
|
|
69
|
+
lines.push(`- Expires: ${finding.suppression.expiresAt}`);
|
|
70
|
+
lines.push("");
|
|
50
71
|
}
|
|
51
|
-
lines.push("");
|
|
52
72
|
}
|
|
53
73
|
return lines.join("\n");
|
|
54
74
|
}
|
|
@@ -55,7 +55,15 @@ export function renderSarifReport(result) {
|
|
|
55
55
|
}
|
|
56
56
|
: {}),
|
|
57
57
|
...(finding.evidence ? { properties: { evidence: finding.evidence } } : {})
|
|
58
|
-
}))
|
|
58
|
+
})),
|
|
59
|
+
...(result.suppressionSummary
|
|
60
|
+
? {
|
|
61
|
+
properties: {
|
|
62
|
+
suppressionSummary: result.suppressionSummary,
|
|
63
|
+
suppressedFindings: result.suppressedFindings ?? []
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
: {})
|
|
59
67
|
}
|
|
60
68
|
]
|
|
61
69
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ManagedSuppressionRecord, SuppressionMutationResult } from "../core/suppression-management.js";
|
|
2
|
+
type SuppressionMutationCommand = "suppress.add" | "suppress.remove";
|
|
3
|
+
export declare function renderSuppressionList(configPath: string, suppressions: ManagedSuppressionRecord[]): string;
|
|
4
|
+
export declare function renderSuppressionListJson(configPath: string, suppressions: ManagedSuppressionRecord[]): string;
|
|
5
|
+
export declare function renderSuppressionMutation(command: SuppressionMutationCommand, configPath: string, result: SuppressionMutationResult): string;
|
|
6
|
+
export declare function renderSuppressionMutationJson(command: SuppressionMutationCommand, configPath: string, result: SuppressionMutationResult): string;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
function isObject(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function isInvalidField(value) {
|
|
5
|
+
return (value === "record" ||
|
|
6
|
+
value === "fingerprint" ||
|
|
7
|
+
value === "reason" ||
|
|
8
|
+
value === "expiresAt");
|
|
9
|
+
}
|
|
10
|
+
function isValidSuppressionSummary(value) {
|
|
11
|
+
return (isObject(value) &&
|
|
12
|
+
!("invalidField" in value) &&
|
|
13
|
+
typeof value.fingerprint === "string" &&
|
|
14
|
+
typeof value.reason === "string" &&
|
|
15
|
+
value.reason.trim().length > 0 &&
|
|
16
|
+
typeof value.expiresAt === "string");
|
|
17
|
+
}
|
|
18
|
+
function buildListEntry(record) {
|
|
19
|
+
if (record.status === "invalid" && isInvalidField(record.invalidField)) {
|
|
20
|
+
return {
|
|
21
|
+
index: record.index,
|
|
22
|
+
status: record.status,
|
|
23
|
+
invalidField: record.invalidField
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if ((record.status === "active" || record.status === "expired") &&
|
|
27
|
+
isValidSuppressionSummary(record)) {
|
|
28
|
+
return {
|
|
29
|
+
index: record.index,
|
|
30
|
+
status: record.status,
|
|
31
|
+
fingerprint: record.fingerprint,
|
|
32
|
+
reason: record.reason,
|
|
33
|
+
expiresAt: record.expiresAt
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
index: record.index,
|
|
38
|
+
status: "invalid",
|
|
39
|
+
invalidField: "record"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function buildMutationSummary(value) {
|
|
43
|
+
if (isObject(value) && isInvalidField(value.invalidField)) {
|
|
44
|
+
return {
|
|
45
|
+
invalidField: value.invalidField
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (isValidSuppressionSummary(value)) {
|
|
49
|
+
return {
|
|
50
|
+
fingerprint: value.fingerprint,
|
|
51
|
+
reason: value.reason,
|
|
52
|
+
expiresAt: value.expiresAt
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { invalidField: "record" };
|
|
56
|
+
}
|
|
57
|
+
function getActionLabel(command) {
|
|
58
|
+
return command === "suppress.add" ? "Added" : "Removed";
|
|
59
|
+
}
|
|
60
|
+
export function renderSuppressionList(configPath, suppressions) {
|
|
61
|
+
const lines = [
|
|
62
|
+
`Config: ${configPath}`,
|
|
63
|
+
`Total suppressions: ${suppressions.length}`
|
|
64
|
+
];
|
|
65
|
+
if (suppressions.length === 0) {
|
|
66
|
+
lines.push("", "No suppressions.");
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
lines.push("", "Suppressions", "------------");
|
|
70
|
+
for (const suppression of suppressions) {
|
|
71
|
+
const entry = buildListEntry(suppression);
|
|
72
|
+
if (entry.status === "invalid") {
|
|
73
|
+
lines.push(`[${entry.index}] INVALID ${entry.invalidField}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
lines.push(`[${entry.index}] ${entry.status.toUpperCase()} ${entry.fingerprint}`);
|
|
77
|
+
lines.push(` Reason: ${entry.reason}`);
|
|
78
|
+
lines.push(` Expires: ${entry.expiresAt}`);
|
|
79
|
+
}
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
export function renderSuppressionListJson(configPath, suppressions) {
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
schemaVersion: "1.0.0",
|
|
85
|
+
kind: "doctor.suppress.list",
|
|
86
|
+
command: "suppress.list",
|
|
87
|
+
configPath,
|
|
88
|
+
suppressions: suppressions.map(buildListEntry)
|
|
89
|
+
}, null, 2);
|
|
90
|
+
}
|
|
91
|
+
export function renderSuppressionMutation(command, configPath, result) {
|
|
92
|
+
const summary = buildMutationSummary(result.suppression);
|
|
93
|
+
const lines = [
|
|
94
|
+
`Action: ${getActionLabel(command)}`,
|
|
95
|
+
`Config: ${configPath}`,
|
|
96
|
+
`Index: ${result.index}`
|
|
97
|
+
];
|
|
98
|
+
lines.push("");
|
|
99
|
+
if ("invalidField" in summary) {
|
|
100
|
+
lines.push(`Invalid field: ${summary.invalidField}`);
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
lines.push(`Fingerprint: ${summary.fingerprint}`);
|
|
104
|
+
lines.push(`Reason: ${summary.reason}`);
|
|
105
|
+
lines.push(`Expires: ${summary.expiresAt}`);
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
export function renderSuppressionMutationJson(command, configPath, result) {
|
|
109
|
+
return JSON.stringify({
|
|
110
|
+
schemaVersion: "1.0.0",
|
|
111
|
+
kind: `doctor.${command}`,
|
|
112
|
+
command,
|
|
113
|
+
configPath,
|
|
114
|
+
index: result.index,
|
|
115
|
+
suppression: buildMutationSummary(result.suppression)
|
|
116
|
+
}, null, 2);
|
|
117
|
+
}
|
|
@@ -35,7 +35,10 @@ export function renderTextReport(result, options = {}) {
|
|
|
35
35
|
`Target: ${result.targetPath}`,
|
|
36
36
|
`Summary: ${failCount} fail, ${warnCount} warn, ${totalCount} total`
|
|
37
37
|
];
|
|
38
|
-
if (result.
|
|
38
|
+
if (result.suppressionSummary) {
|
|
39
|
+
lines.push(`Suppressions: ${result.suppressionSummary.applied} applied, ${result.suppressionSummary.expired} expired, ${result.suppressionSummary.invalid} invalid`);
|
|
40
|
+
}
|
|
41
|
+
if (result.findings.length === 0 && !result.suppressedFindings?.length) {
|
|
39
42
|
if (result.runtimeScorecard) {
|
|
40
43
|
lines.push("", "Runtime Scorecard", "----------------");
|
|
41
44
|
lines.push(`initialize: ${result.runtimeScorecard.initialize}`);
|
|
@@ -82,6 +85,16 @@ export function renderTextReport(result, options = {}) {
|
|
|
82
85
|
};
|
|
83
86
|
appendSection("Failures", failures, glyphs.fail);
|
|
84
87
|
appendSection("Warnings", warnings, glyphs.warn);
|
|
88
|
+
if (result.suppressedFindings?.length) {
|
|
89
|
+
lines.push("", "Suppressed Findings", "-------------------");
|
|
90
|
+
for (const finding of result.suppressedFindings) {
|
|
91
|
+
lines.push(`- ${finding.id}`);
|
|
92
|
+
lines.push(` Message: ${finding.message}`);
|
|
93
|
+
lines.push(` Fingerprint: ${finding.fingerprint ?? "unavailable"}`);
|
|
94
|
+
lines.push(` Reason: ${finding.suppression.reason}`);
|
|
95
|
+
lines.push(` Expires: ${finding.suppression.expiresAt}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
85
98
|
if (result.runtimeScorecard) {
|
|
86
99
|
lines.push("", "Runtime Scorecard", "----------------");
|
|
87
100
|
lines.push(`initialize: ${result.runtimeScorecard.initialize}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RuleCategory = "package" | "skill" | "mcp" | "runtime" | "security";
|
|
1
|
+
export type RuleCategory = "package" | "skill" | "mcp" | "runtime" | "security" | "configuration";
|
|
2
2
|
export type RuleSeverity = "fail" | "warn";
|
|
3
3
|
export interface RuleDefinition {
|
|
4
4
|
id: string;
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
export const ruleCatalog = [
|
|
2
|
+
{
|
|
3
|
+
id: "suppression.invalid",
|
|
4
|
+
category: "configuration",
|
|
5
|
+
defaultSeverity: "warn",
|
|
6
|
+
summary: "A targeted suppression record is invalid.",
|
|
7
|
+
why: "Invalid suppression records are not applied and can create false confidence about accepted findings.",
|
|
8
|
+
fix: "Fix or remove the invalid suppression record in `.codex-doctor.json`.",
|
|
9
|
+
example: '{ "fingerprint": "<64 lowercase hex characters>", "reason": "Reviewed exception.", "expiresAt": "2026-07-31" }'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "suppression.expired",
|
|
13
|
+
category: "configuration",
|
|
14
|
+
defaultSeverity: "warn",
|
|
15
|
+
summary: "A targeted suppression has expired.",
|
|
16
|
+
why: "Expired risk acceptance must be reviewed again before the finding can be suppressed.",
|
|
17
|
+
fix: "Remove the expired record or replace it with a newly reviewed expiration date.",
|
|
18
|
+
example: '{ "fingerprint": "<fingerprint>", "reason": "Reviewed again.", "expiresAt": "2026-08-31" }'
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
id: "plugin.manifest.missing",
|
|
4
22
|
category: "package",
|
package/dist/run-cli.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runCheck } from "./index.js";
|
|
2
|
+
import { writeRawDoctorConfig } from "./index.js";
|
|
2
3
|
export interface CliIo {
|
|
3
4
|
writeStdout(message: string): void;
|
|
4
5
|
writeStderr(message: string): void;
|
|
@@ -13,6 +14,8 @@ export interface CliTerminalContext {
|
|
|
13
14
|
export interface RunCliOptions {
|
|
14
15
|
terminalContext?: CliTerminalContext;
|
|
15
16
|
runCheckImpl?: typeof runCheck;
|
|
17
|
+
writeRawDoctorConfigImpl?: typeof writeRawDoctorConfig;
|
|
18
|
+
now?: () => Date;
|
|
16
19
|
releaseAssetUploadImpl?: (args: string[]) => Promise<void>;
|
|
17
20
|
resolveLatestVersion?: () => Promise<string>;
|
|
18
21
|
}
|