verification-layer 0.27.13 → 0.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/COMPLIANCE_DELIVERY.md +161 -0
- package/dist/acknowledgments.d.ts +1 -1
- package/dist/acknowledgments.d.ts.map +1 -1
- package/dist/acknowledgments.js +29 -11
- package/dist/acknowledgments.js.map +1 -1
- package/dist/baseline.d.ts +50 -12
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +165 -23
- package/dist/baseline.js.map +1 -1
- package/dist/delivery/diff.d.ts +10 -0
- package/dist/delivery/diff.d.ts.map +1 -0
- package/dist/delivery/diff.js +41 -0
- package/dist/delivery/diff.js.map +1 -0
- package/dist/delivery/evidence.d.ts +11 -0
- package/dist/delivery/evidence.d.ts.map +1 -0
- package/dist/delivery/evidence.js +87 -0
- package/dist/delivery/evidence.js.map +1 -0
- package/dist/delivery/git.d.ts +8 -0
- package/dist/delivery/git.d.ts.map +1 -0
- package/dist/delivery/git.js +98 -0
- package/dist/delivery/git.js.map +1 -0
- package/dist/delivery/governance.d.ts +23 -0
- package/dist/delivery/governance.d.ts.map +1 -0
- package/dist/delivery/governance.js +118 -0
- package/dist/delivery/governance.js.map +1 -0
- package/dist/delivery/graph.d.ts +11 -0
- package/dist/delivery/graph.d.ts.map +1 -0
- package/dist/delivery/graph.js +130 -0
- package/dist/delivery/graph.js.map +1 -0
- package/dist/delivery/index.d.ts +14 -0
- package/dist/delivery/index.d.ts.map +1 -0
- package/dist/delivery/index.js +10 -0
- package/dist/delivery/index.js.map +1 -0
- package/dist/delivery/policy.d.ts +13 -0
- package/dist/delivery/policy.d.ts.map +1 -0
- package/dist/delivery/policy.js +100 -0
- package/dist/delivery/policy.js.map +1 -0
- package/dist/delivery/remediation.d.ts +9 -0
- package/dist/delivery/remediation.d.ts.map +1 -0
- package/dist/delivery/remediation.js +60 -0
- package/dist/delivery/remediation.js.map +1 -0
- package/dist/delivery/runner.d.ts +26 -0
- package/dist/delivery/runner.d.ts.map +1 -0
- package/dist/delivery/runner.js +79 -0
- package/dist/delivery/runner.js.map +1 -0
- package/dist/delivery/types.d.ts +156 -0
- package/dist/delivery/types.d.ts.map +1 -0
- package/dist/delivery/types.js +2 -0
- package/dist/delivery/types.js.map +1 -0
- package/dist/delivery/upload.d.ts +20 -0
- package/dist/delivery/upload.d.ts.map +1 -0
- package/dist/delivery/upload.js +72 -0
- package/dist/delivery/upload.js.map +1 -0
- package/dist/delivery-cli.d.ts +3 -0
- package/dist/delivery-cli.d.ts.map +1 -0
- package/dist/delivery-cli.js +306 -0
- package/dist/delivery-cli.js.map +1 -0
- package/dist/delivery-upload-cli.d.ts +3 -0
- package/dist/delivery-upload-cli.d.ts.map +1 -0
- package/dist/delivery-upload-cli.js +37 -0
- package/dist/delivery-upload-cli.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/rule-catalog.json +1 -1
- package/package.json +15 -9
- package/templates/github-actions/compliance-gate.yml +61 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# vlayer Compliance Delivery
|
|
2
|
+
|
|
3
|
+
`vlayer` can operate as a change-oriented compliance control in addition to a repository scanner. The delivery layer answers a narrower release question:
|
|
4
|
+
|
|
5
|
+
> Did this software change introduce compliance risk that violates our delivery policy?
|
|
6
|
+
|
|
7
|
+
The `vlayer-ci` binary is designed for pull requests and deployment pipelines. It keeps existing baseline debt visible without allowing historical findings to make every future pull request fail.
|
|
8
|
+
|
|
9
|
+
## 1. Create a portable baseline
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vlayer baseline . -o .vlayer-baseline.json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Baseline format 2.0 stores project-relative paths and stable semantic fingerprints. Line numbers are retained for display but are not part of the fingerprint, so a line shift or a different CI checkout directory does not invalidate the baseline.
|
|
16
|
+
|
|
17
|
+
Commit `.vlayer-baseline.json` with the repository when you want the current accepted state to be the starting point for change detection.
|
|
18
|
+
|
|
19
|
+
## 2. Define delivery policy
|
|
20
|
+
|
|
21
|
+
Create `vlayer.policy.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"blockOn": ["critical", "high"],
|
|
26
|
+
"maxNew": {
|
|
27
|
+
"medium": 3
|
|
28
|
+
},
|
|
29
|
+
"changedFilesOnly": true,
|
|
30
|
+
"minimumScore": 0,
|
|
31
|
+
"requireBaseline": true,
|
|
32
|
+
"failOnExpiredAcknowledgments": true
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Policy can also live at `.vlayer/policy.json` or in a `deliveryPolicy` block inside `.vlayerrc.json`. An explicit `--policy` file takes precedence.
|
|
37
|
+
|
|
38
|
+
### Policy semantics
|
|
39
|
+
|
|
40
|
+
- `blockOn`: any new active finding at one of these severities blocks deployment.
|
|
41
|
+
- `maxNew`: numeric budget for new findings by severity.
|
|
42
|
+
- `changedFilesOnly`: evaluate newly introduced findings only in files changed by the Git comparison when Git metadata is available.
|
|
43
|
+
- `minimumScore`: optional whole-repository compliance score floor.
|
|
44
|
+
- `requireBaseline`: fail when no baseline is available.
|
|
45
|
+
- `failOnExpiredAcknowledgments`: treat expired exceptions as governance failures.
|
|
46
|
+
|
|
47
|
+
Acknowledged and inline-suppressed findings are not considered new active risk. Expired acknowledgments do not suppress findings.
|
|
48
|
+
|
|
49
|
+
## 3. Gate a pull request
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
vlayer-ci guard . \
|
|
53
|
+
--baseline .vlayer-baseline.json \
|
|
54
|
+
--policy vlayer.policy.json \
|
|
55
|
+
--base main \
|
|
56
|
+
--output vlayer-guard.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Exit codes:
|
|
60
|
+
|
|
61
|
+
- `0`: policy passed; deployment is allowed.
|
|
62
|
+
- `2`: scan completed but delivery policy blocked the change.
|
|
63
|
+
- `1`: operational failure.
|
|
64
|
+
|
|
65
|
+
AI triage is disabled by default for deterministic CI decisions. Use `--ai` only when a team deliberately wants model-assisted triage in the gate.
|
|
66
|
+
|
|
67
|
+
## 4. Generate audit evidence
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
vlayer-ci evidence . \
|
|
71
|
+
--baseline .vlayer-baseline.json \
|
|
72
|
+
--policy vlayer.policy.json \
|
|
73
|
+
--base main \
|
|
74
|
+
-o vlayer-evidence.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The evidence package contains:
|
|
78
|
+
|
|
79
|
+
- source-control provenance (repository, base/head SHA, branch, author when available),
|
|
80
|
+
- delivery decision and policy reasons,
|
|
81
|
+
- new and resolved finding counts,
|
|
82
|
+
- normalized finding metadata,
|
|
83
|
+
- mapped HIPAA controls,
|
|
84
|
+
- a code-to-control compliance graph,
|
|
85
|
+
- a SHA-256 package hash.
|
|
86
|
+
|
|
87
|
+
Evidence deliberately excludes source-code context snippets. This reduces the chance that PHI, credentials, or application secrets are copied into an audit artifact.
|
|
88
|
+
|
|
89
|
+
Verify a stored package:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
vlayer-ci verify-evidence vlayer-evidence.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Any modification to the hashed package body causes verification to fail.
|
|
96
|
+
|
|
97
|
+
## 5. Compliance graph
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
vlayer-ci graph . \
|
|
101
|
+
--baseline .vlayer-baseline.json \
|
|
102
|
+
--base main \
|
|
103
|
+
-o vlayer-compliance-graph.json \
|
|
104
|
+
--mermaid vlayer-compliance-graph.mmd
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The graph relates:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
code -> finding -> HIPAA control -> delivery policy
|
|
111
|
+
\-> owner
|
|
112
|
+
evidence ----> finding
|
|
113
|
+
evidence ----> policy decision
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This is a machine-readable foundation for workspace reporting, evidence search, and agent reasoning rather than a UI-only diagram.
|
|
117
|
+
|
|
118
|
+
## 6. Exception lifecycle
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
vlayer-ci exceptions . --config .vlayerrc.json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The command reports active, expired, and soon-to-expire acknowledgments. It exits `2` when expired exceptions exist so an organization can make exception review a scheduled governance control.
|
|
125
|
+
|
|
126
|
+
An acknowledgment should include an owner, reason, date, ticket, and expiry whenever practical. Expiry means re-review, not permanent suppression.
|
|
127
|
+
|
|
128
|
+
## 7. Compliance agent
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
vlayer-ci agent . --baseline .vlayer-baseline.json --base main
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The agent command converts the delivery decision into a remediation plan with file, rule, recommendation, and whether a deterministic vlayer fix exists.
|
|
135
|
+
|
|
136
|
+
To apply only registered deterministic fix strategies and then re-run the gate:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
vlayer-ci agent . \
|
|
140
|
+
--baseline .vlayer-baseline.json \
|
|
141
|
+
--base main \
|
|
142
|
+
--apply-safe-fixes \
|
|
143
|
+
--markdown vlayer-remediation.md
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`--apply-safe-fixes` mutates source files and should therefore be invoked deliberately on a working branch. It never authorizes an AI-generated arbitrary code change.
|
|
147
|
+
|
|
148
|
+
## GitHub Actions
|
|
149
|
+
|
|
150
|
+
A reusable example is included at `templates/github-actions/compliance-gate.yml`.
|
|
151
|
+
|
|
152
|
+
The important requirements are:
|
|
153
|
+
|
|
154
|
+
1. Checkout with `fetch-depth: 0` so the base/head comparison is available.
|
|
155
|
+
2. Run `vlayer-ci guard` before deployment.
|
|
156
|
+
3. Generate the evidence package even when the gate blocks.
|
|
157
|
+
4. Upload the guard result and evidence JSON as CI artifacts.
|
|
158
|
+
|
|
159
|
+
## Security model
|
|
160
|
+
|
|
161
|
+
The delivery layer is a static-analysis and policy-enforcement system, not a certification authority. A passing gate means the configured automated controls found no change that violates the configured policy. It is not a legal attestation that an organization is HIPAA compliant.
|
|
@@ -18,5 +18,5 @@ export declare function applyAcknowledgments(findings: Finding[], config: Vlayer
|
|
|
18
18
|
/**
|
|
19
19
|
* Validate acknowledged finding configuration
|
|
20
20
|
*/
|
|
21
|
-
export declare function validateAcknowledgedFinding(ack:
|
|
21
|
+
export declare function validateAcknowledgedFinding(ack: unknown, index: number): string[];
|
|
22
22
|
//# sourceMappingURL=acknowledgments.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acknowledgments.d.ts","sourceRoot":"","sources":["../src/acknowledgments.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,YAAY,GACnB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"acknowledgments.d.ts","sourceRoot":"","sources":["../src/acknowledgments.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,YAAY,GACnB,mBAAmB,CA2DrB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,EAAE,YAAY,GACnB,OAAO,EAAE,CAoBX;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,MAAM,GACZ,MAAM,EAAE,CAuCV"}
|
package/dist/acknowledgments.js
CHANGED
|
@@ -29,16 +29,28 @@ export function checkAcknowledgment(finding, config) {
|
|
|
29
29
|
if (ack.severity && ack.severity !== finding.severity) {
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
|
-
//
|
|
32
|
+
// Expired acknowledgments must never suppress findings. Return the match
|
|
33
|
+
// metadata so callers can still distinguish an expired exception from no
|
|
34
|
+
// matching exception at all.
|
|
33
35
|
const expired = ack.expiresAt ? new Date(ack.expiresAt) < new Date() : false;
|
|
34
|
-
|
|
36
|
+
if (expired) {
|
|
37
|
+
return {
|
|
38
|
+
acknowledged: false,
|
|
39
|
+
reason: ack.reason,
|
|
40
|
+
acknowledgedBy: ack.acknowledgedBy,
|
|
41
|
+
acknowledgedAt: ack.acknowledgedAt,
|
|
42
|
+
ticketUrl: ack.ticketUrl,
|
|
43
|
+
expired: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// All criteria matched and the acknowledgment is still active.
|
|
35
47
|
return {
|
|
36
48
|
acknowledged: true,
|
|
37
49
|
reason: ack.reason,
|
|
38
50
|
acknowledgedBy: ack.acknowledgedBy,
|
|
39
51
|
acknowledgedAt: ack.acknowledgedAt,
|
|
40
52
|
ticketUrl: ack.ticketUrl,
|
|
41
|
-
expired,
|
|
53
|
+
expired: false,
|
|
42
54
|
};
|
|
43
55
|
}
|
|
44
56
|
return { acknowledged: false };
|
|
@@ -70,30 +82,36 @@ export function applyAcknowledgments(findings, config) {
|
|
|
70
82
|
*/
|
|
71
83
|
export function validateAcknowledgedFinding(ack, index) {
|
|
72
84
|
const errors = [];
|
|
73
|
-
|
|
85
|
+
const value = typeof ack === 'object' && ack !== null ? ack : {};
|
|
86
|
+
if (!value.pattern || typeof value.pattern !== 'string') {
|
|
74
87
|
errors.push(`acknowledgedFindings[${index}]: 'pattern' is required and must be a string`);
|
|
75
88
|
}
|
|
76
|
-
if (!
|
|
89
|
+
if (!value.reason || typeof value.reason !== 'string') {
|
|
77
90
|
errors.push(`acknowledgedFindings[${index}]: 'reason' is required and must be a string`);
|
|
78
91
|
}
|
|
79
|
-
if (!
|
|
92
|
+
if (!value.acknowledgedBy || typeof value.acknowledgedBy !== 'string') {
|
|
80
93
|
errors.push(`acknowledgedFindings[${index}]: 'acknowledgedBy' is required and must be a string`);
|
|
81
94
|
}
|
|
82
|
-
if (!
|
|
95
|
+
if (!value.acknowledgedAt || typeof value.acknowledgedAt !== 'string') {
|
|
83
96
|
errors.push(`acknowledgedFindings[${index}]: 'acknowledgedAt' is required and must be a string`);
|
|
84
97
|
}
|
|
85
98
|
else {
|
|
86
99
|
// Validate ISO 8601 date format
|
|
87
|
-
const date = new Date(
|
|
100
|
+
const date = new Date(value.acknowledgedAt);
|
|
88
101
|
if (isNaN(date.getTime())) {
|
|
89
102
|
errors.push(`acknowledgedFindings[${index}]: 'acknowledgedAt' must be a valid ISO 8601 date`);
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
if (isNaN(date.getTime())) {
|
|
105
|
+
if (value.expiresAt !== undefined) {
|
|
106
|
+
if (typeof value.expiresAt !== 'string') {
|
|
95
107
|
errors.push(`acknowledgedFindings[${index}]: 'expiresAt' must be a valid ISO 8601 date`);
|
|
96
108
|
}
|
|
109
|
+
else {
|
|
110
|
+
const date = new Date(value.expiresAt);
|
|
111
|
+
if (isNaN(date.getTime())) {
|
|
112
|
+
errors.push(`acknowledgedFindings[${index}]: 'expiresAt' must be a valid ISO 8601 date`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
97
115
|
}
|
|
98
116
|
return errors;
|
|
99
117
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acknowledgments.js","sourceRoot":"","sources":["../src/acknowledgments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAYtC;;GAEG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,MAAoB;IAEpB,IAAI,CAAC,MAAM,CAAC,oBAAoB,IAAI,MAAM,CAAC,oBAAoB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7E,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IACjC,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,oBAAoB,EAAE,CAAC;QAC9C,yEAAyE;QACzE,2EAA2E;QAC3E,uEAAuE;QACvE,2CAA2C;QAC3C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YACzD,SAAS;QACX,CAAC;QAED,6CAA6C;QAC7C,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;YACX,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;YAC1D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChC,SAAS;YACX,CAAC;QACH,CAAC;QAED,2CAA2C;QAC3C,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtD,SAAS;QACX,CAAC;QAED,2CAA2C;QAC3C,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtD,SAAS;QACX,CAAC;QAED,
|
|
1
|
+
{"version":3,"file":"acknowledgments.js","sourceRoot":"","sources":["../src/acknowledgments.ts"],"names":[],"mappings":"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"}
|
package/dist/baseline.d.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import type { Finding } from './types.js';
|
|
2
|
+
export declare const BASELINE_VERSION = "2.0";
|
|
2
3
|
export interface BaselineEntry {
|
|
4
|
+
/** Stable v2 fingerprint. `hash` is retained for backwards compatibility. */
|
|
3
5
|
hash: string;
|
|
6
|
+
fingerprint?: string;
|
|
7
|
+
/** Canonical rule family without scanner-generated line suffixes. */
|
|
8
|
+
ruleId?: string;
|
|
4
9
|
id: string;
|
|
10
|
+
/** Portable path relative to the scanned project root for v2 baselines. */
|
|
5
11
|
file: string;
|
|
12
|
+
/** Informational only in v2; line number is not part of the fingerprint. */
|
|
6
13
|
line?: number;
|
|
7
14
|
title: string;
|
|
8
15
|
severity: string;
|
|
9
16
|
category: string;
|
|
17
|
+
/** Normalized matched code used to distinguish repeated rule families. */
|
|
18
|
+
anchor?: string;
|
|
10
19
|
}
|
|
11
20
|
export interface Baseline {
|
|
12
21
|
version: string;
|
|
@@ -14,31 +23,60 @@ export interface Baseline {
|
|
|
14
23
|
findings: BaselineEntry[];
|
|
15
24
|
}
|
|
16
25
|
/**
|
|
17
|
-
*
|
|
26
|
+
* Scanner rules historically embedded source line numbers in some finding IDs
|
|
27
|
+
* (for example `enc-des-81`). Those suffixes are location metadata, not rule
|
|
28
|
+
* identity, and therefore must not invalidate a baseline after a refactor.
|
|
29
|
+
* Upper-case catalog IDs such as `CRED-001` and `HIPAA-2026-001` are preserved.
|
|
18
30
|
*/
|
|
19
|
-
export declare function
|
|
31
|
+
export declare function canonicalRuleId(id: string): string;
|
|
20
32
|
/**
|
|
21
|
-
*
|
|
33
|
+
* Build a stable semantic anchor from the matched source line. Context lines
|
|
34
|
+
* are intentionally whitespace-normalized so formatting-only edits do not
|
|
35
|
+
* create a new baseline finding.
|
|
22
36
|
*/
|
|
23
|
-
export declare function
|
|
37
|
+
export declare function getFindingAnchor(finding: Finding): string;
|
|
24
38
|
/**
|
|
25
|
-
*
|
|
39
|
+
* Infer the checkout root from all real finding paths. This keeps the existing
|
|
40
|
+
* `saveBaseline(path, findings)` API portable without requiring callers to pass
|
|
41
|
+
* the scan root. When a caller knows the root, passing it explicitly is still
|
|
42
|
+
* preferred.
|
|
43
|
+
*/
|
|
44
|
+
export declare function inferProjectRoot(findings: Finding[]): string;
|
|
45
|
+
/**
|
|
46
|
+
* Convert a finding path to a portable project-relative path. Virtual project
|
|
47
|
+
* findings retain their stable symbolic names.
|
|
48
|
+
*/
|
|
49
|
+
export declare function normalizeFindingFile(finding: Finding, projectRoot?: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Generate the v2 finding fingerprint. Source line number is deliberately not
|
|
52
|
+
* included. The fingerprint is based on portable file path, canonical rule
|
|
53
|
+
* family, stable title, and matched-code anchor.
|
|
54
|
+
*/
|
|
55
|
+
export declare function generateFindingHash(finding: Finding, projectRoot?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Create a baseline entry from a finding.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createBaselineEntry(finding: Finding, projectRoot?: string): BaselineEntry;
|
|
60
|
+
/**
|
|
61
|
+
* Load baseline from file.
|
|
26
62
|
*/
|
|
27
63
|
export declare function loadBaseline(path: string): Promise<Baseline | null>;
|
|
28
64
|
/**
|
|
29
|
-
* Save baseline
|
|
65
|
+
* Save a portable v2 baseline. `projectRoot` is optional for API compatibility;
|
|
66
|
+
* when omitted it is inferred from the absolute finding paths.
|
|
30
67
|
*/
|
|
31
|
-
export declare function saveBaseline(path: string, findings: Finding[]): Promise<void>;
|
|
68
|
+
export declare function saveBaseline(path: string, findings: Finding[], projectRoot?: string): Promise<void>;
|
|
32
69
|
/**
|
|
33
|
-
* Check if a finding exists in
|
|
70
|
+
* Check if a finding exists in a baseline.
|
|
34
71
|
*/
|
|
35
|
-
export declare function isInBaseline(finding: Finding, baseline: Baseline): boolean;
|
|
72
|
+
export declare function isInBaseline(finding: Finding, baseline: Baseline, projectRoot?: string): boolean;
|
|
36
73
|
/**
|
|
37
|
-
* Apply baseline to findings
|
|
74
|
+
* Apply baseline to findings. The root is inferred once for stable v2 hashing,
|
|
75
|
+
* making a committed baseline portable between developer machines and CI.
|
|
38
76
|
*/
|
|
39
|
-
export declare function applyBaseline(findings: Finding[], baseline: Baseline | null): Finding[];
|
|
77
|
+
export declare function applyBaseline(findings: Finding[], baseline: Baseline | null, projectRoot?: string): Finding[];
|
|
40
78
|
/**
|
|
41
|
-
* Get statistics about baseline application
|
|
79
|
+
* Get statistics about baseline application.
|
|
42
80
|
*/
|
|
43
81
|
export declare function getBaselineStats(findings: Finding[]): {
|
|
44
82
|
total: number;
|
package/dist/baseline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AAQtC,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAMD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAWzD;AAWD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAkB5D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAYnF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAUlF;AAUD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,aAAa,CAczF;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAWzE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,OAAO,EAAE,EACnB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CASf;AAwBD;;GAEG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,EAClB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAmBT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,QAAQ,GAAG,IAAI,EACzB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,EAAE,CAaX;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb,CASA"}
|
package/dist/baseline.js
CHANGED
|
@@ -1,35 +1,140 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'path';
|
|
4
|
+
export const BASELINE_VERSION = '2.0';
|
|
5
|
+
const VIRTUAL_FILES = new Set([
|
|
6
|
+
'project-level',
|
|
7
|
+
'ASSET-INVENTORY',
|
|
8
|
+
'PHI-FLOW-MAP',
|
|
9
|
+
]);
|
|
10
|
+
function normalizeSlashes(value) {
|
|
11
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scanner rules historically embedded source line numbers in some finding IDs
|
|
15
|
+
* (for example `enc-des-81`). Those suffixes are location metadata, not rule
|
|
16
|
+
* identity, and therefore must not invalidate a baseline after a refactor.
|
|
17
|
+
* Upper-case catalog IDs such as `CRED-001` and `HIPAA-2026-001` are preserved.
|
|
18
|
+
*/
|
|
19
|
+
export function canonicalRuleId(id) {
|
|
20
|
+
if (/^[a-z][a-z0-9-]*-\d+$/.test(id)) {
|
|
21
|
+
return id.replace(/-\d+$/, '');
|
|
22
|
+
}
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a stable semantic anchor from the matched source line. Context lines
|
|
27
|
+
* are intentionally whitespace-normalized so formatting-only edits do not
|
|
28
|
+
* create a new baseline finding.
|
|
29
|
+
*/
|
|
30
|
+
export function getFindingAnchor(finding) {
|
|
31
|
+
if (!finding.context || finding.context.length === 0)
|
|
32
|
+
return '';
|
|
33
|
+
const matched = finding.context.filter(line => line.isMatch);
|
|
34
|
+
const source = matched.length > 0 ? matched : finding.context;
|
|
35
|
+
return source
|
|
36
|
+
.map(line => line.content.trim().toLowerCase().replace(/\s+/g, ' '))
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join('\n')
|
|
39
|
+
.slice(0, 1000);
|
|
40
|
+
}
|
|
41
|
+
function isVirtualFile(file) {
|
|
42
|
+
return VIRTUAL_FILES.has(file);
|
|
43
|
+
}
|
|
44
|
+
function isInside(root, file) {
|
|
45
|
+
const rel = relative(root, file);
|
|
46
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Infer the checkout root from all real finding paths. This keeps the existing
|
|
50
|
+
* `saveBaseline(path, findings)` API portable without requiring callers to pass
|
|
51
|
+
* the scan root. When a caller knows the root, passing it explicitly is still
|
|
52
|
+
* preferred.
|
|
53
|
+
*/
|
|
54
|
+
export function inferProjectRoot(findings) {
|
|
55
|
+
const files = findings
|
|
56
|
+
.map(f => f.file)
|
|
57
|
+
.filter(file => isAbsolute(file) && !isVirtualFile(file))
|
|
58
|
+
.map(file => resolve(file));
|
|
59
|
+
if (files.length === 0)
|
|
60
|
+
return process.cwd();
|
|
61
|
+
let root = dirname(files[0]);
|
|
62
|
+
for (const file of files.slice(1)) {
|
|
63
|
+
while (!isInside(root, file)) {
|
|
64
|
+
const parent = dirname(root);
|
|
65
|
+
if (parent === root)
|
|
66
|
+
break;
|
|
67
|
+
root = parent;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return root;
|
|
71
|
+
}
|
|
3
72
|
/**
|
|
4
|
-
*
|
|
73
|
+
* Convert a finding path to a portable project-relative path. Virtual project
|
|
74
|
+
* findings retain their stable symbolic names.
|
|
5
75
|
*/
|
|
6
|
-
export function
|
|
7
|
-
|
|
8
|
-
|
|
76
|
+
export function normalizeFindingFile(finding, projectRoot) {
|
|
77
|
+
if (isVirtualFile(finding.file))
|
|
78
|
+
return finding.file;
|
|
79
|
+
if (projectRoot && isAbsolute(finding.file)) {
|
|
80
|
+
const root = resolve(projectRoot);
|
|
81
|
+
const file = resolve(finding.file);
|
|
82
|
+
if (isInside(root, file)) {
|
|
83
|
+
return normalizeSlashes(relative(root, file));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return normalizeSlashes(finding.file);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Generate the v2 finding fingerprint. Source line number is deliberately not
|
|
90
|
+
* included. The fingerprint is based on portable file path, canonical rule
|
|
91
|
+
* family, stable title, and matched-code anchor.
|
|
92
|
+
*/
|
|
93
|
+
export function generateFindingHash(finding, projectRoot) {
|
|
94
|
+
const key = [
|
|
95
|
+
'v2',
|
|
96
|
+
normalizeFindingFile(finding, projectRoot),
|
|
97
|
+
canonicalRuleId(finding.id),
|
|
98
|
+
finding.title.trim().toLowerCase(),
|
|
99
|
+
getFindingAnchor(finding),
|
|
100
|
+
].join('|');
|
|
101
|
+
return createHash('sha256').update(key).digest('hex').substring(0, 16);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Legacy v1 hash used only to keep exact same-checkout baselines readable.
|
|
105
|
+
*/
|
|
106
|
+
function generateLegacyFindingHash(finding) {
|
|
9
107
|
const key = `${finding.file}:${finding.line || 0}:${finding.id}:${finding.title}`;
|
|
10
108
|
return createHash('sha256').update(key).digest('hex').substring(0, 16);
|
|
11
109
|
}
|
|
12
110
|
/**
|
|
13
|
-
* Create a baseline entry from a finding
|
|
111
|
+
* Create a baseline entry from a finding.
|
|
14
112
|
*/
|
|
15
|
-
export function createBaselineEntry(finding) {
|
|
113
|
+
export function createBaselineEntry(finding, projectRoot) {
|
|
114
|
+
const fingerprint = generateFindingHash(finding, projectRoot);
|
|
16
115
|
return {
|
|
17
|
-
hash:
|
|
116
|
+
hash: fingerprint,
|
|
117
|
+
fingerprint,
|
|
118
|
+
ruleId: canonicalRuleId(finding.id),
|
|
18
119
|
id: finding.id,
|
|
19
|
-
file: finding
|
|
120
|
+
file: normalizeFindingFile(finding, projectRoot),
|
|
20
121
|
line: finding.line,
|
|
21
122
|
title: finding.title,
|
|
22
123
|
severity: finding.severity,
|
|
23
124
|
category: finding.category,
|
|
125
|
+
anchor: getFindingAnchor(finding) || undefined,
|
|
24
126
|
};
|
|
25
127
|
}
|
|
26
128
|
/**
|
|
27
|
-
* Load baseline from file
|
|
129
|
+
* Load baseline from file.
|
|
28
130
|
*/
|
|
29
131
|
export async function loadBaseline(path) {
|
|
30
132
|
try {
|
|
31
133
|
const content = await readFile(path, 'utf-8');
|
|
32
134
|
const baseline = JSON.parse(content);
|
|
135
|
+
if (!baseline || !Array.isArray(baseline.findings) || typeof baseline.version !== 'string') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
33
138
|
return baseline;
|
|
34
139
|
}
|
|
35
140
|
catch {
|
|
@@ -37,32 +142,69 @@ export async function loadBaseline(path) {
|
|
|
37
142
|
}
|
|
38
143
|
}
|
|
39
144
|
/**
|
|
40
|
-
* Save baseline
|
|
145
|
+
* Save a portable v2 baseline. `projectRoot` is optional for API compatibility;
|
|
146
|
+
* when omitted it is inferred from the absolute finding paths.
|
|
41
147
|
*/
|
|
42
|
-
export async function saveBaseline(path, findings) {
|
|
148
|
+
export async function saveBaseline(path, findings, projectRoot) {
|
|
149
|
+
const root = projectRoot ? resolve(projectRoot) : inferProjectRoot(findings);
|
|
43
150
|
const baseline = {
|
|
44
|
-
version:
|
|
151
|
+
version: BASELINE_VERSION,
|
|
45
152
|
createdAt: new Date().toISOString(),
|
|
46
|
-
findings: findings.map(createBaselineEntry),
|
|
153
|
+
findings: findings.map(finding => createBaselineEntry(finding, root)),
|
|
47
154
|
};
|
|
48
155
|
await writeFile(path, JSON.stringify(baseline, null, 2), 'utf-8');
|
|
49
156
|
}
|
|
157
|
+
function pathMatches(entryFile, findingFile) {
|
|
158
|
+
const expected = normalizeSlashes(entryFile);
|
|
159
|
+
const actual = normalizeSlashes(findingFile);
|
|
160
|
+
if (VIRTUAL_FILES.has(expected) || VIRTUAL_FILES.has(actual)) {
|
|
161
|
+
return expected === actual;
|
|
162
|
+
}
|
|
163
|
+
return actual === expected || actual.endsWith(`/${expected}`);
|
|
164
|
+
}
|
|
165
|
+
function semanticEntryMatches(finding, entry) {
|
|
166
|
+
if (!pathMatches(entry.file, finding.file))
|
|
167
|
+
return false;
|
|
168
|
+
if ((entry.ruleId ?? canonicalRuleId(entry.id)) !== canonicalRuleId(finding.id))
|
|
169
|
+
return false;
|
|
170
|
+
if (entry.title.trim().toLowerCase() !== finding.title.trim().toLowerCase())
|
|
171
|
+
return false;
|
|
172
|
+
const anchor = getFindingAnchor(finding);
|
|
173
|
+
if (entry.anchor && anchor)
|
|
174
|
+
return entry.anchor === anchor;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
50
177
|
/**
|
|
51
|
-
* Check if a finding exists in
|
|
178
|
+
* Check if a finding exists in a baseline.
|
|
52
179
|
*/
|
|
53
|
-
export function isInBaseline(finding, baseline) {
|
|
54
|
-
|
|
55
|
-
|
|
180
|
+
export function isInBaseline(finding, baseline, projectRoot) {
|
|
181
|
+
if (baseline.version.startsWith('2')) {
|
|
182
|
+
if (projectRoot) {
|
|
183
|
+
const fingerprint = generateFindingHash(finding, projectRoot);
|
|
184
|
+
if (baseline.findings.some(entry => (entry.fingerprint ?? entry.hash) === fingerprint)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Portable fallback: useful when callers do not know the checkout root.
|
|
189
|
+
return baseline.findings.some(entry => semanticEntryMatches(finding, entry));
|
|
190
|
+
}
|
|
191
|
+
// v1 files remain readable on the checkout where they were generated.
|
|
192
|
+
const legacyHash = generateLegacyFindingHash(finding);
|
|
193
|
+
if (baseline.findings.some(entry => entry.hash === legacyHash))
|
|
194
|
+
return true;
|
|
195
|
+
// Best-effort portability for legacy files while users regenerate to v2.
|
|
196
|
+
return baseline.findings.some(entry => semanticEntryMatches(finding, entry));
|
|
56
197
|
}
|
|
57
198
|
/**
|
|
58
|
-
* Apply baseline to findings
|
|
199
|
+
* Apply baseline to findings. The root is inferred once for stable v2 hashing,
|
|
200
|
+
* making a committed baseline portable between developer machines and CI.
|
|
59
201
|
*/
|
|
60
|
-
export function applyBaseline(findings, baseline) {
|
|
61
|
-
if (!baseline)
|
|
202
|
+
export function applyBaseline(findings, baseline, projectRoot) {
|
|
203
|
+
if (!baseline)
|
|
62
204
|
return findings;
|
|
63
|
-
|
|
205
|
+
const root = projectRoot ?? inferProjectRoot(findings);
|
|
64
206
|
return findings.map(finding => {
|
|
65
|
-
if (isInBaseline(finding, baseline)) {
|
|
207
|
+
if (isInBaseline(finding, baseline, root)) {
|
|
66
208
|
return {
|
|
67
209
|
...finding,
|
|
68
210
|
isBaseline: true,
|
|
@@ -72,7 +214,7 @@ export function applyBaseline(findings, baseline) {
|
|
|
72
214
|
});
|
|
73
215
|
}
|
|
74
216
|
/**
|
|
75
|
-
* Get statistics about baseline application
|
|
217
|
+
* Get statistics about baseline application.
|
|
76
218
|
*/
|
|
77
219
|
export function getBaselineStats(findings) {
|
|
78
220
|
const baseline = findings.filter(f => f.isBaseline).length;
|
package/dist/baseline.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Finding } from '../types.js';
|
|
2
|
+
import type { Baseline } from '../baseline.js';
|
|
3
|
+
import type { FindingDelta } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Build the change-oriented view used by CI policy evaluation. Findings already
|
|
6
|
+
* marked by `scan(..., baselineFile)` are treated as existing debt; active
|
|
7
|
+
* findings outside the baseline are new risk.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildFindingDelta(findings: Finding[], projectRoot: string, changedFiles: string[], baseline: Baseline | null): FindingDelta;
|
|
10
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/delivery/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAc/C;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EAAE,EACtB,QAAQ,EAAE,QAAQ,GAAG,IAAI,GACxB,YAAY,CA2Bd"}
|