@stratum-hq/compliance 0.1.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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/finding.d.ts +24 -0
- package/dist/finding.d.ts.map +1 -0
- package/dist/finding.js +41 -0
- package/dist/finding.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/scoring.d.ts +52 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +70 -0
- package/dist/scoring.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christian Crank
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @stratum-hq/compliance
|
|
2
|
+
|
|
3
|
+
A content-free compliance **kernel** for [Stratum](https://github.com/stratum-hq/Stratum): the pure mechanics and type shapes any compliance product needs, with none of the content. Zero runtime dependencies, no database, no network, no provider, and no built-in catalog.
|
|
4
|
+
|
|
5
|
+
It gives you three things:
|
|
6
|
+
|
|
7
|
+
1. **Coverage scoring** — diff a declared baseline against a resolved state and get a per-control breakdown plus a 0 to 100 score.
|
|
8
|
+
2. **A finding state machine** — a pure decision for whether a new evaluation outcome should open, resolve, or leave a finding alone.
|
|
9
|
+
3. **A control type vocabulary** — structural interfaces for describing a catalog of controls.
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> This is **not** a batteries-included compliance solution. It ships no frameworks, no controls, no provider mappings, and no thresholds. **Bring your own catalog:** you supply the content and the persistence; this package supplies the arithmetic and the shapes.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @stratum-hq/compliance
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Coverage scoring
|
|
21
|
+
|
|
22
|
+
`scoreCoverage` compares a `baseline` (what each control is expected to be) against a `resolved` map (what each control actually is). Only baseline keys are scored; extra resolved keys are ignored.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { scoreCoverage } from "@stratum-hq/compliance";
|
|
26
|
+
|
|
27
|
+
const baseline = { mfaRequired: true, passwordMinLength: 12 };
|
|
28
|
+
const resolved = {
|
|
29
|
+
mfaRequired: { value: "true" }, // resolved config often arrives as strings
|
|
30
|
+
passwordMinLength: { value: 8 },
|
|
31
|
+
// passwordMinLength is present but wrong; a key omitted here would be "missing"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const result = scoreCoverage(baseline, resolved);
|
|
35
|
+
// {
|
|
36
|
+
// total: 2, compliant: 1, drift: 1, missing: 0, score: 50,
|
|
37
|
+
// details: [
|
|
38
|
+
// { key: "mfaRequired", expected: true, actual: "true", status: "compliant" },
|
|
39
|
+
// { key: "passwordMinLength", expected: 12, actual: 8, status: "drift" },
|
|
40
|
+
// ],
|
|
41
|
+
// }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `status` is `"compliant"` (matches), `"drift"` (present but wrong), or `"missing"` (no resolved entry for that key).
|
|
45
|
+
- `score` is `round(compliant / total * 100)`. An **empty baseline scores 100** — nothing is required, so nothing is out of compliance.
|
|
46
|
+
|
|
47
|
+
### Equality
|
|
48
|
+
|
|
49
|
+
The default comparator is `looseEqual`: booleans, numbers, and strings compare by their **textual form** (`true` equals `"true"`, `5` equals `"5"`), while objects and arrays compare by their **JSON form** (order-sensitive). This tolerates config values that come back as strings. Override it for strict or domain-specific comparison:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { scoreCoverage, looseEqual } from "@stratum-hq/compliance";
|
|
53
|
+
|
|
54
|
+
scoreCoverage(baseline, resolved, { equals: (e, a) => e === a }); // strict
|
|
55
|
+
looseEqual(true, "true"); // => true
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Finding state machine
|
|
59
|
+
|
|
60
|
+
`reconcileFinding` decides what should happen to a control's finding when a new evaluation outcome arrives. It is a pure decision — you persist the result.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { reconcileFinding } from "@stratum-hq/compliance";
|
|
64
|
+
|
|
65
|
+
reconcileFinding("fail", "none"); // { type: "open" } — new gap
|
|
66
|
+
reconcileFinding("fail", "open"); // { type: "noop" } — already tracked
|
|
67
|
+
reconcileFinding("pass", "open"); // { type: "resolve" } — gap closed
|
|
68
|
+
reconcileFinding("pass", "accepted"); // { type: "noop" } — accepted risk untouched
|
|
69
|
+
reconcileFinding("na", "open"); // { type: "noop" } — na/error never change a finding
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The rules:
|
|
73
|
+
|
|
74
|
+
- `fail` **opens** a finding, unless one is already active (`open` / `remediating`) or the risk was formally `accepted`.
|
|
75
|
+
- `pass` **resolves** an active finding (`open` / `remediating`), and leaves an `accepted` finding untouched; otherwise there is nothing to resolve.
|
|
76
|
+
- `na` and `error` are always no-ops.
|
|
77
|
+
|
|
78
|
+
Map `{ type: "open" }` and `{ type: "resolve" }` onto your own inserts and updates — the state machine has no opinion about storage.
|
|
79
|
+
|
|
80
|
+
## Type vocabulary
|
|
81
|
+
|
|
82
|
+
Structural, content-free interfaces for describing a catalog. They carry no data and no provider mapping; populate them with your own content.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import type {
|
|
86
|
+
FieldType, // "boolean" | "number" | "enum"
|
|
87
|
+
Verification, // "verified" | "attestation" | "declared"
|
|
88
|
+
CatalogField,
|
|
89
|
+
CatalogGroup,
|
|
90
|
+
ManualControl,
|
|
91
|
+
ControlDef,
|
|
92
|
+
} from "@stratum-hq/compliance";
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Design
|
|
96
|
+
|
|
97
|
+
Everything here is a pure function or a type. No side effects, no IO, no global state, and nothing to configure. That is deliberate: the reusable part of compliance is the arithmetic and the vocabulary, so the interesting, product-specific parts (which controls, mapped to which real settings, for which framework, loaded and stored how) stay entirely on your side of the boundary.
|
|
98
|
+
|
|
99
|
+
## Links
|
|
100
|
+
|
|
101
|
+
- GitHub: https://github.com/stratum-hq/Stratum
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT © Christian Crank
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The outcome of evaluating a single control. */
|
|
2
|
+
export type ControlOutcome = "pass" | "fail" | "na" | "error";
|
|
3
|
+
/** The current state of a control's finding. `none` means no finding exists. */
|
|
4
|
+
export type FindingState = "open" | "remediating" | "resolved" | "accepted" | "none";
|
|
5
|
+
export type FindingAction = {
|
|
6
|
+
type: "open";
|
|
7
|
+
} | {
|
|
8
|
+
type: "resolve";
|
|
9
|
+
} | {
|
|
10
|
+
type: "noop";
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Decide what should happen to a control's finding given the new evaluation
|
|
14
|
+
* outcome and the current finding state. Pure.
|
|
15
|
+
*
|
|
16
|
+
* Rules:
|
|
17
|
+
* - `fail` opens a finding, unless one is already active (`open` /
|
|
18
|
+
* `remediating`) or the risk was formally `accepted`.
|
|
19
|
+
* - `pass` resolves an active finding (`open` / `remediating`), and leaves an
|
|
20
|
+
* `accepted` finding untouched; there is nothing to resolve otherwise.
|
|
21
|
+
* - `na` and `error` never change a finding.
|
|
22
|
+
*/
|
|
23
|
+
export declare function reconcileFinding(newOutcome: ControlOutcome, currentState: FindingState): FindingAction;
|
|
24
|
+
//# sourceMappingURL=finding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../src/finding.ts"],"names":[],"mappings":"AAQA,kDAAkD;AAClD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC;AAE9D,gFAAgF;AAChF,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,aAAa,GACb,UAAU,GACV,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,EAC1B,YAAY,EAAE,YAAY,GACzB,aAAa,CAsBf"}
|
package/dist/finding.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Finding state machine.
|
|
3
|
+
//
|
|
4
|
+
// A pure decision function for reconciling a control's finding when a new
|
|
5
|
+
// evaluation outcome arrives. It decides only what should happen (open a
|
|
6
|
+
// finding, resolve the active one, or do nothing); persisting that decision is
|
|
7
|
+
// the caller's job. This keeps the transition rules testable in isolation from
|
|
8
|
+
// any storage.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.reconcileFinding = reconcileFinding;
|
|
11
|
+
/**
|
|
12
|
+
* Decide what should happen to a control's finding given the new evaluation
|
|
13
|
+
* outcome and the current finding state. Pure.
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - `fail` opens a finding, unless one is already active (`open` /
|
|
17
|
+
* `remediating`) or the risk was formally `accepted`.
|
|
18
|
+
* - `pass` resolves an active finding (`open` / `remediating`), and leaves an
|
|
19
|
+
* `accepted` finding untouched; there is nothing to resolve otherwise.
|
|
20
|
+
* - `na` and `error` never change a finding.
|
|
21
|
+
*/
|
|
22
|
+
function reconcileFinding(newOutcome, currentState) {
|
|
23
|
+
switch (newOutcome) {
|
|
24
|
+
case "fail":
|
|
25
|
+
if (currentState === "open" ||
|
|
26
|
+
currentState === "remediating" ||
|
|
27
|
+
currentState === "accepted") {
|
|
28
|
+
return { type: "noop" };
|
|
29
|
+
}
|
|
30
|
+
return { type: "open" };
|
|
31
|
+
case "pass":
|
|
32
|
+
if (currentState === "open" || currentState === "remediating") {
|
|
33
|
+
return { type: "resolve" };
|
|
34
|
+
}
|
|
35
|
+
return { type: "noop" };
|
|
36
|
+
case "na":
|
|
37
|
+
case "error":
|
|
38
|
+
return { type: "noop" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=finding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding.js","sourceRoot":"","sources":["../src/finding.ts"],"names":[],"mappings":";AAAA,yBAAyB;AACzB,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,+EAA+E;AAC/E,+EAA+E;AAC/E,eAAe;;AA6Bf,4CAyBC;AApCD;;;;;;;;;;GAUG;AACH,SAAgB,gBAAgB,CAC9B,UAA0B,EAC1B,YAA0B;IAE1B,QAAQ,UAAU,EAAE,CAAC;QACnB,KAAK,MAAM;YACT,IACE,YAAY,KAAK,MAAM;gBACvB,YAAY,KAAK,aAAa;gBAC9B,YAAY,KAAK,UAAU,EAC3B,CAAC;gBACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;YAC1B,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAE1B,KAAK,MAAM;YACT,IAAI,YAAY,KAAK,MAAM,IAAI,YAAY,KAAK,aAAa,EAAE,CAAC;gBAC9D,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YAC7B,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAE1B,KAAK,IAAI,CAAC;QACV,KAAK,OAAO;YACV,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC5B,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { scoreCoverage, looseEqual } from "./scoring.js";
|
|
2
|
+
export type { ControlStatus, ScoredControl, CoverageResult, ScoreOptions, } from "./scoring.js";
|
|
3
|
+
export { reconcileFinding } from "./finding.js";
|
|
4
|
+
export type { ControlOutcome, FindingState, FindingAction } from "./finding.js";
|
|
5
|
+
export type { FieldType, Verification, CatalogField, CatalogGroup, ManualControl, ControlDef, } from "./types.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACzD,YAAY,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,YAAY,GACb,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEhF,YAAY,EACV,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @stratum-hq/compliance
|
|
3
|
+
//
|
|
4
|
+
// A content-free compliance kernel: baseline coverage scoring, a finding state
|
|
5
|
+
// machine, and a control type vocabulary. Pure TypeScript, zero runtime
|
|
6
|
+
// dependencies, no database, no provider, no built-in catalog. Bring your own
|
|
7
|
+
// catalog and persistence; this package supplies only the mechanics and shapes.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.reconcileFinding = exports.looseEqual = exports.scoreCoverage = void 0;
|
|
10
|
+
var scoring_js_1 = require("./scoring.js");
|
|
11
|
+
Object.defineProperty(exports, "scoreCoverage", { enumerable: true, get: function () { return scoring_js_1.scoreCoverage; } });
|
|
12
|
+
Object.defineProperty(exports, "looseEqual", { enumerable: true, get: function () { return scoring_js_1.looseEqual; } });
|
|
13
|
+
var finding_js_1 = require("./finding.js");
|
|
14
|
+
Object.defineProperty(exports, "reconcileFinding", { enumerable: true, get: function () { return finding_js_1.reconcileFinding; } });
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,yBAAyB;AACzB,EAAE;AACF,+EAA+E;AAC/E,wEAAwE;AACxE,8EAA8E;AAC9E,gFAAgF;;;AAEhF,2CAAyD;AAAhD,2GAAA,aAAa,OAAA;AAAE,wGAAA,UAAU,OAAA;AAQlC,2CAAgD;AAAvC,8GAAA,gBAAgB,OAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** The computed state of one control against its baseline. */
|
|
2
|
+
export type ControlStatus = "compliant" | "drift" | "missing";
|
|
3
|
+
/** One control's contribution to a {@link CoverageResult}. */
|
|
4
|
+
export interface ScoredControl {
|
|
5
|
+
key: string;
|
|
6
|
+
expected: unknown;
|
|
7
|
+
/** The resolved value, or `undefined` when the control is missing. */
|
|
8
|
+
actual: unknown;
|
|
9
|
+
status: ControlStatus;
|
|
10
|
+
}
|
|
11
|
+
export interface CoverageResult {
|
|
12
|
+
/** Number of controls in the baseline. */
|
|
13
|
+
total: number;
|
|
14
|
+
compliant: number;
|
|
15
|
+
drift: number;
|
|
16
|
+
missing: number;
|
|
17
|
+
/** 0 to 100, the share of baseline controls that are compliant. */
|
|
18
|
+
score: number;
|
|
19
|
+
details: ScoredControl[];
|
|
20
|
+
}
|
|
21
|
+
export interface ScoreOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Value equality. Defaults to {@link looseEqual} (booleans / numbers /
|
|
24
|
+
* strings compare by textual form, objects by JSON). Callers can override,
|
|
25
|
+
* for example to compare strictly or with domain-specific tolerance.
|
|
26
|
+
*/
|
|
27
|
+
equals?: (expected: unknown, actual: unknown) => boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The default string-loose comparator, exported for reuse.
|
|
31
|
+
*
|
|
32
|
+
* Objects and arrays compare by their JSON form; every other value (booleans,
|
|
33
|
+
* numbers, strings, `null`, `undefined`) compares by its textual form, so
|
|
34
|
+
* `true` equals `"true"` and `5` equals `"5"`. This tolerates the common case
|
|
35
|
+
* where a resolved config value arrives as a string even though its baseline
|
|
36
|
+
* was declared as a boolean or number. Note JSON comparison is order-sensitive:
|
|
37
|
+
* objects whose keys are in a different order are treated as unequal.
|
|
38
|
+
*/
|
|
39
|
+
export declare function looseEqual(expected: unknown, actual: unknown): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Diff a declared baseline against a resolved value map. Pure. No IO.
|
|
42
|
+
*
|
|
43
|
+
* Only the keys present in `baseline` are scored; extra keys in `resolved` are
|
|
44
|
+
* ignored. A baseline key with no matching entry in `resolved` is `missing`; a
|
|
45
|
+
* matching entry whose value satisfies `equals` is `compliant`, otherwise it is
|
|
46
|
+
* `drift`. An empty baseline scores 100 (nothing is required, so nothing is out
|
|
47
|
+
* of compliance).
|
|
48
|
+
*/
|
|
49
|
+
export declare function scoreCoverage(baseline: Record<string, unknown>, resolved: Record<string, {
|
|
50
|
+
value: unknown;
|
|
51
|
+
}>, options?: ScoreOptions): CoverageResult;
|
|
52
|
+
//# sourceMappingURL=scoring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../src/scoring.ts"],"names":[],"mappings":"AAQA,8DAA8D;AAC9D,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,CAAC;AAE9D,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CAC1D;AAMD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAMtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,EAC5C,OAAO,CAAC,EAAE,YAAY,GACrB,cAAc,CA+BhB"}
|
package/dist/scoring.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Baseline coverage scoring.
|
|
3
|
+
//
|
|
4
|
+
// A pure, content-free primitive for any product that measures a resolved state
|
|
5
|
+
// against a declared baseline. Given a baseline map (what each control is
|
|
6
|
+
// expected to be) and a resolved map (what each control actually is), it reports
|
|
7
|
+
// per-control compliant / drift / missing and an overall coverage score. It
|
|
8
|
+
// knows nothing about frameworks, providers, or where the values came from.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.looseEqual = looseEqual;
|
|
11
|
+
exports.scoreCoverage = scoreCoverage;
|
|
12
|
+
function isObjectLike(value) {
|
|
13
|
+
return typeof value === "object" && value !== null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The default string-loose comparator, exported for reuse.
|
|
17
|
+
*
|
|
18
|
+
* Objects and arrays compare by their JSON form; every other value (booleans,
|
|
19
|
+
* numbers, strings, `null`, `undefined`) compares by its textual form, so
|
|
20
|
+
* `true` equals `"true"` and `5` equals `"5"`. This tolerates the common case
|
|
21
|
+
* where a resolved config value arrives as a string even though its baseline
|
|
22
|
+
* was declared as a boolean or number. Note JSON comparison is order-sensitive:
|
|
23
|
+
* objects whose keys are in a different order are treated as unequal.
|
|
24
|
+
*/
|
|
25
|
+
function looseEqual(expected, actual) {
|
|
26
|
+
if (isObjectLike(expected) || isObjectLike(actual)) {
|
|
27
|
+
if (!isObjectLike(expected) || !isObjectLike(actual))
|
|
28
|
+
return false;
|
|
29
|
+
return JSON.stringify(expected) === JSON.stringify(actual);
|
|
30
|
+
}
|
|
31
|
+
return String(expected) === String(actual);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Diff a declared baseline against a resolved value map. Pure. No IO.
|
|
35
|
+
*
|
|
36
|
+
* Only the keys present in `baseline` are scored; extra keys in `resolved` are
|
|
37
|
+
* ignored. A baseline key with no matching entry in `resolved` is `missing`; a
|
|
38
|
+
* matching entry whose value satisfies `equals` is `compliant`, otherwise it is
|
|
39
|
+
* `drift`. An empty baseline scores 100 (nothing is required, so nothing is out
|
|
40
|
+
* of compliance).
|
|
41
|
+
*/
|
|
42
|
+
function scoreCoverage(baseline, resolved, options) {
|
|
43
|
+
const equals = options?.equals ?? looseEqual;
|
|
44
|
+
const details = [];
|
|
45
|
+
let compliant = 0;
|
|
46
|
+
let drift = 0;
|
|
47
|
+
let missing = 0;
|
|
48
|
+
for (const key of Object.keys(baseline)) {
|
|
49
|
+
const expected = baseline[key];
|
|
50
|
+
const entry = resolved[key];
|
|
51
|
+
if (entry === undefined) {
|
|
52
|
+
missing++;
|
|
53
|
+
details.push({ key, expected, actual: undefined, status: "missing" });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const actual = entry.value;
|
|
57
|
+
if (equals(expected, actual)) {
|
|
58
|
+
compliant++;
|
|
59
|
+
details.push({ key, expected, actual, status: "compliant" });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
drift++;
|
|
63
|
+
details.push({ key, expected, actual, status: "drift" });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const total = details.length;
|
|
67
|
+
const score = total === 0 ? 100 : Math.round((compliant / total) * 100);
|
|
68
|
+
return { total, compliant, drift, missing, score, details };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=scoring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoring.js","sourceRoot":"","sources":["../src/scoring.ts"],"names":[],"mappings":";AAAA,6BAA6B;AAC7B,EAAE;AACF,gFAAgF;AAChF,0EAA0E;AAC1E,iFAAiF;AACjF,4EAA4E;AAC5E,4EAA4E;;AAgD5E,gCAMC;AAWD,sCAmCC;AAlED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,UAAU,CAAC,QAAiB,EAAE,MAAe;IAC3D,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAC;QACnE,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,aAAa,CAC3B,QAAiC,EACjC,QAA4C,EAC5C,OAAsB;IAEtB,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,UAAU,CAAC;IAC7C,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAE5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;YACtE,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC;QAC3B,IAAI,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;YAC7B,SAAS,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;QAC/D,CAAC;aAAM,CAAC;YACN,KAAK,EAAE,CAAC;YACR,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;IAC7B,MAAM,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;IAExE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC9D,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** The data type of a catalog field's value. */
|
|
2
|
+
export type FieldType = "boolean" | "number" | "enum";
|
|
3
|
+
/**
|
|
4
|
+
* How a control's compliance is established: machine-`verified` from evidence,
|
|
5
|
+
* covered by a signed operator `attestation`, or self-`declared` by the caller.
|
|
6
|
+
*/
|
|
7
|
+
export type Verification = "verified" | "attestation" | "declared";
|
|
8
|
+
/** A single configurable control in a catalog. */
|
|
9
|
+
export interface CatalogField {
|
|
10
|
+
key: string;
|
|
11
|
+
label: string;
|
|
12
|
+
/** The {@link CatalogGroup} this field belongs to, by name. */
|
|
13
|
+
group: string;
|
|
14
|
+
type: FieldType;
|
|
15
|
+
/** Choices for an `enum` field. */
|
|
16
|
+
options?: {
|
|
17
|
+
value: string;
|
|
18
|
+
label: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** Suggested values for a `number` field. */
|
|
21
|
+
presets?: {
|
|
22
|
+
value: number;
|
|
23
|
+
label: string;
|
|
24
|
+
}[];
|
|
25
|
+
help: string;
|
|
26
|
+
verification: Verification;
|
|
27
|
+
/** For a `declared` field, the key of the verified control it maps to. */
|
|
28
|
+
verifiedControl?: string;
|
|
29
|
+
}
|
|
30
|
+
/** A named grouping of catalog fields. */
|
|
31
|
+
export interface CatalogGroup {
|
|
32
|
+
name: string;
|
|
33
|
+
blurb: string;
|
|
34
|
+
}
|
|
35
|
+
/** A control that no automated check can prove, covered by an attestation. */
|
|
36
|
+
export interface ManualControl {
|
|
37
|
+
key: string;
|
|
38
|
+
label: string;
|
|
39
|
+
/** The frameworks this manual control satisfies. */
|
|
40
|
+
frameworks: string[];
|
|
41
|
+
description: string;
|
|
42
|
+
}
|
|
43
|
+
/** A verified control definition. */
|
|
44
|
+
export interface ControlDef {
|
|
45
|
+
key: string;
|
|
46
|
+
label: string;
|
|
47
|
+
/** The declared field this control backs, or `null` if it stands alone. */
|
|
48
|
+
declaredKey: string | null;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,gDAAgD;AAChD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtD;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,UAAU,CAAC;AAEnE,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,mCAAmC;IACnC,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,6CAA6C;IAC7C,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qCAAqC;AACrC,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Control type vocabulary.
|
|
3
|
+
//
|
|
4
|
+
// Structural, content-free shapes for describing a catalog of controls: how a
|
|
5
|
+
// field is typed, how it is verified, and how declared controls relate to
|
|
6
|
+
// verified ones. These interfaces carry no data and no provider mapping. Bring
|
|
7
|
+
// your own catalog: populate them with your product's own content.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,2BAA2B;AAC3B,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,+EAA+E;AAC/E,mEAAmE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stratum-hq/compliance",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Content-free compliance kernel for Stratum: baseline coverage scoring, a finding state machine, and a control type vocabulary",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"compliance",
|
|
7
|
+
"coverage",
|
|
8
|
+
"posture",
|
|
9
|
+
"drift",
|
|
10
|
+
"state-machine",
|
|
11
|
+
"control",
|
|
12
|
+
"audit",
|
|
13
|
+
"typescript",
|
|
14
|
+
"stratum"
|
|
15
|
+
],
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"clean": "rm -rf dist"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.4.0",
|
|
42
|
+
"vitest": "^1.6.0"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "Christian Crank",
|
|
46
|
+
"homepage": "https://github.com/stratum-hq/Stratum/tree/main/packages/compliance#readme",
|
|
47
|
+
"bugs": "https://github.com/stratum-hq/Stratum/issues",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.0.0"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/stratum-hq/Stratum.git",
|
|
54
|
+
"directory": "packages/compliance"
|
|
55
|
+
}
|
|
56
|
+
}
|