@fjall/util 35.0.0 → 36.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/posturePolicy/index.d.ts +13 -0
- package/dist/posturePolicy/index.js +1 -0
- package/dist/posturePolicy/issueStatusLabels.d.ts +83 -0
- package/dist/posturePolicy/issueStatusLabels.js +1 -0
- package/dist/posturePolicy/machineLane.d.ts +13 -0
- package/dist/posturePolicy/machineLane.js +1 -0
- package/dist/posturePolicy/registry.d.ts +202 -0
- package/dist/posturePolicy/registry.js +1 -0
- package/dist/posturePolicy/tierConsent.d.ts +62 -0
- package/dist/posturePolicy/tierConsent.js +1 -0
- package/package.json +5 -1
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
129 files minified at 2026-09-10T10:35:24.637Z
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@fjall/util/posturePolicy` — the tier-aware alert policy's client-safe
|
|
3
|
+
* data and words (design 2026-09-01-tier-aware-alert-policy § 13): the
|
|
4
|
+
* expected and mitigated registries, the status-label table, the machine
|
|
5
|
+
* lanes and the sentence builders the create surfaces print. No node
|
|
6
|
+
* built-ins: the webapp imports this subpath in browser code and the CLI in
|
|
7
|
+
* its Ink screens. The webapp's scan predicate, the rule titles and the
|
|
8
|
+
* lane writers' actor sentinel stay server-side in the webapp.
|
|
9
|
+
*/
|
|
10
|
+
export { ISSUE_STATUS_LABELS, countClause, listStatusClauses, storedStatusClause, type IssueStatusLabels } from "./issueStatusLabels.js";
|
|
11
|
+
export { MACHINE_LANE_STATUSES, isMachineLaneStatus, type MachineLaneStatus } from "./machineLane.js";
|
|
12
|
+
export { DEFAULT_POLICY_REGISTRY, EXPECTED_LANE_STAGES, EXPECTED_POLICY, MITIGATED_POLICY, cureClause, evidenceClause, expectedRowCause, findPolicyRow, findPolicyRowByKey, isExpectedLaneStage, laneGroupHeading, policyRowKey, policyRowPresentation, type ExpectedLaneStage, type ExpectedPolicyRow, type MitigatedPolicyRow, type PolicyRegistry, type PolicyResourceScope, type PolicyRowLookup, type PolicyRowMatch, type PolicyRowPresentation } from "./registry.js";
|
|
13
|
+
export { TIER_CONSENT_NOTE_LABEL, expectedLaneWhereClause, expectedRowsAtDefault, tierConsentLead, tierConsentLine, type TierConsentLead, type TierConsentLine } from "./tierConsent.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ISSUE_STATUS_LABELS as o,countClause as E,listStatusClauses as a,storedStatusClause as s}from"./issueStatusLabels.js";import{MACHINE_LANE_STATUSES as i,isMachineLaneStatus as C}from"./machineLane.js";import{DEFAULT_POLICY_REGISTRY as r,EXPECTED_LANE_STAGES as L,EXPECTED_POLICY as u,MITIGATED_POLICY as T,cureClause as l,evidenceClause as _,expectedRowCause as c,findPolicyRow as p,findPolicyRowByKey as A,isExpectedLaneStage as d,laneGroupHeading as I,policyRowKey as R,policyRowPresentation as x}from"./registry.js";import{TIER_CONSENT_NOTE_LABEL as f,expectedLaneWhereClause as y,expectedRowsAtDefault as w,tierConsentLead as N,tierConsentLine as D}from"./tierConsent.js";export{r as DEFAULT_POLICY_REGISTRY,L as EXPECTED_LANE_STAGES,u as EXPECTED_POLICY,o as ISSUE_STATUS_LABELS,i as MACHINE_LANE_STATUSES,T as MITIGATED_POLICY,f as TIER_CONSENT_NOTE_LABEL,E as countClause,l as cureClause,_ as evidenceClause,y as expectedLaneWhereClause,c as expectedRowCause,w as expectedRowsAtDefault,p as findPolicyRow,A as findPolicyRowByKey,d as isExpectedLaneStage,C as isMachineLaneStatus,I as laneGroupHeading,a as listStatusClauses,R as policyRowKey,x as policyRowPresentation,s as storedStatusClause,N as tierConsentLead,D as tierConsentLine};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words a ComplianceIssue status is shown with — the one home for that
|
|
3
|
+
* copy. `../complianceIssue` owns the vocabulary's meaning
|
|
4
|
+
* (`COMPLIANCE_ISSUE_STATUSES`); this table renders it, and every surface
|
|
5
|
+
* that prints a status — the webapp's badges, panels, digest and export, the
|
|
6
|
+
* CLI's create-time consent note — takes the words from here, so two clients
|
|
7
|
+
* cannot disagree on a sentence. Client-safe.
|
|
8
|
+
*
|
|
9
|
+
* Three renderings per status, because they do not collapse into one (alert
|
|
10
|
+
* policy design § 13.11 (e)): the badge is a single capitalised word beside
|
|
11
|
+
* a finding; the clause is a predicate that reads after "is" and after a
|
|
12
|
+
* count ("this finding is expected at this stage", "3 expected at this
|
|
13
|
+
* stage"); the heading is what a lane's rows sit under on the asset panel,
|
|
14
|
+
* the lane digest and the CSV export. The two machine lanes never share a
|
|
15
|
+
* word (ruled). A full `Record` over the vocabulary, so a status added to
|
|
16
|
+
* it fails here at typecheck, at the one place its copy is authored.
|
|
17
|
+
*/
|
|
18
|
+
import { type ComplianceIssueStatus } from "../complianceIssue.js";
|
|
19
|
+
export interface IssueStatusLabels {
|
|
20
|
+
/** One capitalised word, the badge beside a finding. */
|
|
21
|
+
badge: string;
|
|
22
|
+
/** Lower-case predicate: reads after "is" and after a count. */
|
|
23
|
+
clause: string;
|
|
24
|
+
/**
|
|
25
|
+
* The heading a lane's rows sit under on the asset panel and the digest,
|
|
26
|
+
* and the export's count column for that lane. `null` for `open`: its rows
|
|
27
|
+
* are the work band and sit under no heading. (A verifying row joins that
|
|
28
|
+
* band on the panel — the band is the scoring set, the webapp's
|
|
29
|
+
* `issue-display-groups` — yet keeps a heading for its export column.)
|
|
30
|
+
*/
|
|
31
|
+
heading: string | null;
|
|
32
|
+
}
|
|
33
|
+
export declare const ISSUE_STATUS_LABELS: {
|
|
34
|
+
readonly open: {
|
|
35
|
+
readonly badge: "Open";
|
|
36
|
+
readonly clause: "open";
|
|
37
|
+
readonly heading: null;
|
|
38
|
+
};
|
|
39
|
+
readonly acknowledged: {
|
|
40
|
+
readonly badge: "Acknowledged";
|
|
41
|
+
readonly clause: "acknowledged";
|
|
42
|
+
readonly heading: "Acknowledged";
|
|
43
|
+
};
|
|
44
|
+
readonly pending_verification: {
|
|
45
|
+
readonly badge: "Verifying";
|
|
46
|
+
readonly clause: "awaiting verification";
|
|
47
|
+
readonly heading: "Awaiting verification";
|
|
48
|
+
};
|
|
49
|
+
readonly resolved: {
|
|
50
|
+
readonly badge: "Resolved";
|
|
51
|
+
readonly clause: "resolved";
|
|
52
|
+
readonly heading: "Resolved";
|
|
53
|
+
};
|
|
54
|
+
readonly suppressed: {
|
|
55
|
+
readonly badge: "Suppressed";
|
|
56
|
+
readonly clause: "suppressed";
|
|
57
|
+
readonly heading: "Suppressed";
|
|
58
|
+
};
|
|
59
|
+
readonly expected: {
|
|
60
|
+
readonly badge: "Expected";
|
|
61
|
+
readonly clause: "expected at this stage";
|
|
62
|
+
readonly heading: "Expected at this stage";
|
|
63
|
+
};
|
|
64
|
+
readonly mitigated: {
|
|
65
|
+
readonly badge: "Mitigated";
|
|
66
|
+
readonly clause: "mitigated";
|
|
67
|
+
readonly heading: "Mitigated";
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The clause of a stored status, for a message that names the state a row
|
|
72
|
+
* is in. A stored value outside the vocabulary is a later writer's: it is
|
|
73
|
+
* shown quoted as the token it is, never dressed as one of ours.
|
|
74
|
+
*/
|
|
75
|
+
export declare function storedStatusClause(status: string): string;
|
|
76
|
+
/** `"N <clause>"` — the count clause every conformance figure's sublabel is built from. */
|
|
77
|
+
export declare function countClause(count: number, status: ComplianceIssueStatus): string;
|
|
78
|
+
/**
|
|
79
|
+
* The statuses' clauses as one list in vocabulary order — "a, b or c" —
|
|
80
|
+
* whatever order the caller holds them in, so a sentence derived from a
|
|
81
|
+
* policy set reads the same as the set's own declaration order changes.
|
|
82
|
+
*/
|
|
83
|
+
export declare function listStatusClauses(statuses: readonly ComplianceIssueStatus[]): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var c=Object.defineProperty;var t=(e,a)=>c(e,"name",{value:a,configurable:!0});import{COMPLIANCE_ISSUE_STATUSES as g,isComplianceIssueStatus as u}from"../complianceIssue.js";const i={open:{badge:"Open",clause:"open",heading:null},acknowledged:{badge:"Acknowledged",clause:"acknowledged",heading:"Acknowledged"},pending_verification:{badge:"Verifying",clause:"awaiting verification",heading:"Awaiting verification"},resolved:{badge:"Resolved",clause:"resolved",heading:"Resolved"},suppressed:{badge:"Suppressed",clause:"suppressed",heading:"Suppressed"},expected:{badge:"Expected",clause:"expected at this stage",heading:"Expected at this stage"},mitigated:{badge:"Mitigated",clause:"mitigated",heading:"Mitigated"}};function r(e){return u(e)?i[e].clause:`"${e}"`}t(r,"storedStatusClause");function S(e,a){return`${e} ${i[a].clause}`}t(S,"countClause");const d=new Map(g.map((e,a)=>[e,a]));function f(e){const a=[...new Set(e)].sort((s,o)=>(d.get(s)??0)-(d.get(o)??0)).map(s=>i[s].clause),n=a.pop();return n===void 0?"":a.length===0?n:`${a.join(", ")} or ${n}`}t(f,"listStatusClauses");export{i as ISSUE_STATUS_LABELS,S as countClause,f as listStatusClauses,r as storedStatusClause};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine lanes of the alert policy (design
|
|
3
|
+
* 2026-09-01-tier-aware-alert-policy § 13): the two statuses only the scan
|
|
4
|
+
* writes, each spoken for by one registry row (`./registry`). The lane
|
|
5
|
+
* arbitration's write type, the digest's read predicate and the asset
|
|
6
|
+
* panel's grouping all derive from this pair, so a third lane reaches each
|
|
7
|
+
* of them at once. Lives beside the registry because `PolicyRowMatch` is
|
|
8
|
+
* typed on it; the webapp's status classifier re-exports it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MACHINE_LANE_STATUSES: readonly ["expected", "mitigated"];
|
|
11
|
+
export type MachineLaneStatus = (typeof MACHINE_LANE_STATUSES)[number];
|
|
12
|
+
/** Narrows a stored status to a machine lane — the read guard of every lane surface. */
|
|
13
|
+
export declare function isMachineLaneStatus(status: string): status is MachineLaneStatus;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var E=Object.defineProperty;var e=(t,n)=>E(t,"name",{value:n,configurable:!0});const S=["expected","mitigated"],a=new Set(S);function i(t){return a.has(t)}e(i,"isMachineLaneStatus");export{S as MACHINE_LANE_STATUSES,i as isMachineLaneStatus};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The expected and mitigated lanes of the alert policy (design
|
|
3
|
+
* 2026-09-01-tier-aware-alert-policy § 13). Two static registries keyed by
|
|
4
|
+
* rule id, client-safe (no node built-ins) and shared by every surface that
|
|
5
|
+
* speaks for a finding: the webapp's scan predicate, asset panel, digest and
|
|
6
|
+
* export, and the CLI's create-time consent note.
|
|
7
|
+
*
|
|
8
|
+
* - `EXPECTED_POLICY`: findings a construct knob's default deliberately
|
|
9
|
+
* produces on Fjall-managed infrastructure. Softened to `expected` only on
|
|
10
|
+
* a development- or staging-stage account (`EXPECTED_LANE_STAGES`) of the
|
|
11
|
+
* account tier; on every other stage, on an organisation- or platform-tier
|
|
12
|
+
* account, and for an organisation that set posture to strict, they flag.
|
|
13
|
+
* - `MITIGATED_POLICY`: findings a compensating control already meets
|
|
14
|
+
* (§ 13.4a). Never a finding at any stage or tier, counted as met in both
|
|
15
|
+
* scores, visible with the evidence pointer.
|
|
16
|
+
*
|
|
17
|
+
* Everything unlisted flags, and every predicate failure flags
|
|
18
|
+
* (fail-toward-flag). The predicate is the webapp's, server-side
|
|
19
|
+
* (`app/.server/services/compliance-rules/expectedPolicy.ts`): it reads the
|
|
20
|
+
* scan's policy context, which no client has. The rule ids are the webapp's
|
|
21
|
+
* rule registry's; its tests cross-check every row against it, and against
|
|
22
|
+
* the remediation edit each cure names.
|
|
23
|
+
*/
|
|
24
|
+
import type { AccountStage } from "../environments.js";
|
|
25
|
+
import type { BackupTierDefaults } from "../backupTiers.js";
|
|
26
|
+
import type { MachineLaneStatus } from "./machineLane.js";
|
|
27
|
+
/** Account stages on which an expected-lane finding is softened. */
|
|
28
|
+
export declare const EXPECTED_LANE_STAGES: readonly ["development", "staging"];
|
|
29
|
+
export type ExpectedLaneStage = (typeof EXPECTED_LANE_STAGES)[number];
|
|
30
|
+
export declare function isExpectedLaneStage(stage: AccountStage): stage is ExpectedLaneStage;
|
|
31
|
+
/**
|
|
32
|
+
* Narrows a row to the resources it speaks for, by one of two signals:
|
|
33
|
+
*
|
|
34
|
+
* - `logicalIdContains`: CloudFormation tags each resource with its logical
|
|
35
|
+
* id, which the scan stores as `iacInfo.logicalId`; the id is the construct
|
|
36
|
+
* path joined with a trailing hash, so the row matches on the construct's
|
|
37
|
+
* own id as a substring. Only for an id nothing but that construct can
|
|
38
|
+
* carry — a fragment a customer's own construct could share
|
|
39
|
+
* (`assetsBucket`) would match their production uploads bucket.
|
|
40
|
+
* - `tag`: a tag the construct stamps on the resource, read from the asset's
|
|
41
|
+
* stored tags. The key alone matches on presence; with `value`, on that
|
|
42
|
+
* value. An asset whose tags the scan has never read (`tags: null`)
|
|
43
|
+
* matches nothing, and a partial read keeps the prior tags, so a throttled
|
|
44
|
+
* scan neither gains nor loses a row here.
|
|
45
|
+
*/
|
|
46
|
+
export type PolicyResourceScope = {
|
|
47
|
+
logicalIdContains: string;
|
|
48
|
+
} | {
|
|
49
|
+
tag: {
|
|
50
|
+
key: string;
|
|
51
|
+
value?: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* A `BACKUP_TIER_DEFAULTS` column whose `false` produces a K1 finding: the
|
|
56
|
+
* knob the app's backup tier owns and the construct reads through
|
|
57
|
+
* `tierDefault`. Boolean columns only — the K1 rows are cost-saving defaults
|
|
58
|
+
* that are off, so "off" is the one polarity the registry speaks.
|
|
59
|
+
*/
|
|
60
|
+
type BackupTierFindingKnob = {
|
|
61
|
+
[K in keyof BackupTierDefaults]: BackupTierDefaults[K] extends boolean ? K : never;
|
|
62
|
+
}[keyof BackupTierDefaults];
|
|
63
|
+
export interface ExpectedPolicyRow {
|
|
64
|
+
ruleId: string;
|
|
65
|
+
/** Present on a resource-scoped row: for that resource a scoped row wins over the rule's unscoped row. */
|
|
66
|
+
resource?: PolicyResourceScope;
|
|
67
|
+
/** The knob whose default causes the finding: what the user sets, and on which construct. Groups findings by cause on the asset page. */
|
|
68
|
+
knob: {
|
|
69
|
+
name: string;
|
|
70
|
+
construct: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Where the knob is a tier (`backup.tier`): the tier-defaults column that
|
|
74
|
+
* decides the finding, `false` producing it. The create-app wizard and
|
|
75
|
+
* the CLI's create flow read it to list, for a chosen tier, the rows the
|
|
76
|
+
* tier leaves at that default (`./tierConsent`); a row without one is
|
|
77
|
+
* produced at every tier.
|
|
78
|
+
*/
|
|
79
|
+
tierDefault?: BackupTierFindingKnob;
|
|
80
|
+
/**
|
|
81
|
+
* What the knob's default chose and why it is cheaper. A lower-case clause
|
|
82
|
+
* that reads after "Expected on a development-stage account: ", no
|
|
83
|
+
* trailing full stop.
|
|
84
|
+
*/
|
|
85
|
+
expectation: string;
|
|
86
|
+
/**
|
|
87
|
+
* The cure exactly as the user writes it in `infrastructure.ts`: the one
|
|
88
|
+
* edit the rule's RemediationHint applies, so this clause and the Remediate
|
|
89
|
+
* button name the same change (`${edit.property}: ${formatEditValue(edit.value)}`
|
|
90
|
+
* — a string value quoted, as the PR writes it — pinned by the webapp's
|
|
91
|
+
* expectedPolicy.test). Where the knob is a tier (`backup.tier`) the cure is
|
|
92
|
+
* deliberately the per-construct override the remediation lane writes, not
|
|
93
|
+
* the tier: the heading names the cause, the cure names the edit.
|
|
94
|
+
*/
|
|
95
|
+
cure: string;
|
|
96
|
+
/** A high-severity finding is softened only with this per-row opt-in (§ 5c); critical never is. */
|
|
97
|
+
allowHigh?: true;
|
|
98
|
+
}
|
|
99
|
+
export interface MitigatedPolicyRow {
|
|
100
|
+
ruleId: string;
|
|
101
|
+
resource?: PolicyResourceScope;
|
|
102
|
+
/**
|
|
103
|
+
* The resource the control covers — a short noun phrase ("cold-tier
|
|
104
|
+
* bucket"; capitals only for an acronym, "ISR cache bucket") that reads
|
|
105
|
+
* after "Mitigated — " as the row's cause on every surface that heads its
|
|
106
|
+
* group. The S3 rows take it from their exemption control, so a stamp
|
|
107
|
+
* value without a name fails typecheck like one without a control.
|
|
108
|
+
*/
|
|
109
|
+
name: string;
|
|
110
|
+
/** The compensating control. A lower-case clause that reads after "Mitigated: ", no trailing full stop. */
|
|
111
|
+
control: string;
|
|
112
|
+
/** Where a reader verifies the control. */
|
|
113
|
+
evidence: string;
|
|
114
|
+
allowHigh?: true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The K1 rows: findings a construct's cost-saving default produces on
|
|
118
|
+
* Fjall-managed infrastructure by design. A rule two constructs produce for
|
|
119
|
+
* two causes carries two rows — the construct with a fixed path scoped by
|
|
120
|
+
* its logical id, the open set unscoped — and the scoped row wins.
|
|
121
|
+
*/
|
|
122
|
+
export declare const EXPECTED_POLICY: readonly ExpectedPolicyRow[];
|
|
123
|
+
/**
|
|
124
|
+
* The K4 rows: a shape Fjall's own engineering chose for one construct at
|
|
125
|
+
* every tier, met by a compensating control. Each is scoped to the resources
|
|
126
|
+
* its construct produces (`PolicyResourceScope`); precedence among scoped
|
|
127
|
+
* rows is `findPolicyRow`'s. The S3 rows are one per value of the
|
|
128
|
+
* versioning-exemption stamp, in the stamp's own order (the pinned wire
|
|
129
|
+
* order, not a precedence — a tag carries one value), and the stamp is the
|
|
130
|
+
* whole contract: a bucket a construct left unversioned before the stamp
|
|
131
|
+
* existed (constructs < 33.1.0) falls to the K1 row until it is redeployed —
|
|
132
|
+
* an existing mitigated row reopens as `no_mitigation_row`, the reason the
|
|
133
|
+
* customer reads — because the stamp is what tells posture the omission is
|
|
134
|
+
* by design.
|
|
135
|
+
*/
|
|
136
|
+
export declare const MITIGATED_POLICY: readonly MitigatedPolicyRow[];
|
|
137
|
+
export interface PolicyRegistry {
|
|
138
|
+
expected: readonly ExpectedPolicyRow[];
|
|
139
|
+
mitigated: readonly MitigatedPolicyRow[];
|
|
140
|
+
}
|
|
141
|
+
export declare const DEFAULT_POLICY_REGISTRY: PolicyRegistry;
|
|
142
|
+
export type PolicyRowMatch = {
|
|
143
|
+
lane: Extract<MachineLaneStatus, "expected">;
|
|
144
|
+
row: ExpectedPolicyRow;
|
|
145
|
+
} | {
|
|
146
|
+
lane: Extract<MachineLaneStatus, "mitigated">;
|
|
147
|
+
row: MitigatedPolicyRow;
|
|
148
|
+
};
|
|
149
|
+
export interface PolicyRowLookup {
|
|
150
|
+
ruleId: string;
|
|
151
|
+
/** `iacInfo.logicalId`: the CloudFormation logical id the resource carries, or `null`. */
|
|
152
|
+
logicalId: string | null;
|
|
153
|
+
/** The asset's stored tags, or `null` when the scan has never read them — a tag-scoped row then matches nothing. */
|
|
154
|
+
tags: Readonly<Record<string, string>> | null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The row that speaks for this finding. A resource-scoped row wins over an
|
|
158
|
+
* unscoped one across both registries; at equal scope a mitigated row wins
|
|
159
|
+
* over an expected one, a compensating control being the stronger claim;
|
|
160
|
+
* among scoped rows of one lane, registry order. Nothing matches: the rule
|
|
161
|
+
* flags.
|
|
162
|
+
*/
|
|
163
|
+
export declare function findPolicyRow(input: PolicyRowLookup, registry?: PolicyRegistry): PolicyRowMatch | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* The identity of a registry row, stable across the wire: lane, rule and
|
|
166
|
+
* scope. `findPolicyRowByKey` inverts it. The pair is what lets a finding
|
|
167
|
+
* carry its row to the client as one string and the digest fold a lane's
|
|
168
|
+
* findings under one heading per row — a cause with many findings reads as
|
|
169
|
+
* one cause, the cure or the evidence written once.
|
|
170
|
+
*/
|
|
171
|
+
export declare function policyRowKey(match: PolicyRowMatch): string;
|
|
172
|
+
/** The row a `policyRowKey` names, or `undefined` once the registry no longer carries it. */
|
|
173
|
+
export declare function findPolicyRowByKey(key: string, registry?: PolicyRegistry): PolicyRowMatch | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* What a surface writes once for a row's group — the cause its heading
|
|
176
|
+
* carries (an expected row's knob, and the construct it sits on; a
|
|
177
|
+
* mitigated row's `name`, the resource its control covers) and the cure or
|
|
178
|
+
* the evidence. Derived here so the overview digest (server) and the asset
|
|
179
|
+
* panel (client) present a row identically. Every registry row has a cause;
|
|
180
|
+
* `cause` is null only where a surface presents an uncovered group itself.
|
|
181
|
+
*/
|
|
182
|
+
export interface PolicyRowPresentation {
|
|
183
|
+
cause: string | null;
|
|
184
|
+
cure?: string;
|
|
185
|
+
evidence?: string;
|
|
186
|
+
}
|
|
187
|
+
export declare function policyRowPresentation(match: PolicyRowMatch): PolicyRowPresentation;
|
|
188
|
+
/** The cause an expected row's findings are grouped under: the knob, and the construct it sits on. */
|
|
189
|
+
export declare function expectedRowCause(row: ExpectedPolicyRow): string;
|
|
190
|
+
/** The cure clause of an expected row, worded once for the reason, the digest and the asset panel. */
|
|
191
|
+
export declare function cureClause(row: ExpectedPolicyRow): string;
|
|
192
|
+
/** The evidence clause of a mitigated row, worded once for the reason, the digest and the asset panel. */
|
|
193
|
+
export declare function evidenceClause(row: MitigatedPolicyRow): string;
|
|
194
|
+
/**
|
|
195
|
+
* The heading a lane's findings sit under, on the overview digest, the
|
|
196
|
+
* asset panel and the export alike: the lane's own heading from the
|
|
197
|
+
* status-label table (the two lanes share no word), then the cause — an
|
|
198
|
+
* expected row's knob, a mitigated row's resource. Only an uncovered group
|
|
199
|
+
* has no cause, so only it is headed by the lane word alone.
|
|
200
|
+
*/
|
|
201
|
+
export declare function laneGroupHeading(lane: MachineLaneStatus, cause: string | null): string;
|
|
202
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var g=Object.defineProperty;var r=(e,t)=>g(e,"name",{value:t,configurable:!0});import{KMS_RETENTION_TAG_KEY as b,S3_VERSIONING_EXEMPT_TAG_KEY as d,S3_VERSIONING_EXEMPTIONS as m}from"../aws/index.js";import{ISSUE_STATUS_LABELS as I}from"./issueStatusLabels.js";const y=["development","staging"];function C(e){return y.includes(e)}r(C,"isExpectedLaneStage");const E=[{ruleId:"SNS_TOPIC_NOT_ENCRYPTED",resource:{logicalIdContains:"AlarmTopicAlarmNotificationsAlarmNotificationsTopic"},knob:{name:"alarmTopicEncryption",construct:"the alarm topic"},allowHigh:!0,expectation:"the alarm topic is unencrypted because alarmTopicEncryption is off (a customer-managed key costs about $1 to $3 a month per account and region)",cure:"alarmTopicEncryption: true"},{ruleId:"SNS_TOPIC_NOT_ENCRYPTED",knob:{name:"backup.tier",construct:"the app"},tierDefault:"customerManagedKeys",allowHigh:!0,expectation:"the topic is unencrypted because the app's backup tier does not encrypt topics (the enterprise tier's shared customer-managed key costs about $1 to $3 a month per stack)",cure:'encryption: "SSE_KMS"'},{ruleId:"ECS_CONTAINER_INSIGHTS_DISABLED",knob:{name:"backup.tier",construct:"the app"},tierDefault:"containerInsights",expectation:"Container Insights is off because the app's backup tier does not enable it (it bills per metric per cluster)",cure:"containerInsights: true"},{ruleId:"S3_VERSIONING_DISABLED",knob:{name:"backup.tier",construct:"the app"},tierDefault:"s3Versioned",expectation:"bucket versioning is off because the app's backup tier does not version buckets (every overwrite would keep a billed copy)",cure:"versioned: true"}],k={"build-output":{name:"build-output bucket",control:"the bucket holds OpenNext build output that every deploy rewrites and prunes, so versioning would retain a billed copy of each superseded build; the build is reproduced from the commit that made it",site:"createAssetsBucket in fjall/components/infrastructure/lib/patterns/aws/payload.ts",stamped:"on the assets bucket it leaves unversioned"},"isr-cache":{name:"ISR cache bucket",control:"the bucket is the ISR cache Next.js regenerates at runtime, so versioning would retain a billed copy of every regenerated page; a lost entry is rendered again on the next request",site:"createCacheBucket in fjall/components/infrastructure/lib/patterns/aws/payload.ts",stamped:"on the cache bucket it leaves unversioned"},"cold-tier":{name:"cold-tier bucket",control:"the cold tier is MergeTree tiered storage owned by the database, so parts are written and deleted continuously and object versioning would retain every deleted part at unbounded cost; recoverability is the ClickHouse backup lane",site:"ClickHouseColdTierBucket in fjall/components/infrastructure/lib/patterns/aws/clickhouseDatabase.ts",stamped:"(versioned: false by design), the versioned backup bucket beside it"},"access-logs":{name:"access-log bucket",control:"the bucket is the stack's access-log delivery target (application load balancer and CloudFront access logs, expired after 90 days by its lifecycle rule), so versioning would only multiply expired objects",site:"stackAccessLogBucket in fjall/components/infrastructure/lib/resources/aws/logging/accessLogBucket.ts",stamped:"(versioned: false, ACCESS_LOG_RETENTION_DAYS expiry)"}},h=[...m.map(e=>{const{name:t,control:n,site:i,stamped:a}=k[e];return{ruleId:"S3_VERSIONING_DISABLED",resource:{tag:{key:d,value:e}},name:t,control:n,evidence:`${i} stamps ${d}=${e} ${a}`}}),{ruleId:"KMS_KEY_ORPHAN",resource:{tag:{key:b}},name:"retained customer key",control:"the key was retained on purpose (D17) so existing ciphertext stays decryptable; scheduling deletion would render that data unreadable",evidence:"CustomerManagedKey in fjall/components/infrastructure/lib/resources/aws/secrets/kms.ts stamps fjall:kms:retention with the protects scope on every retained key"}],l={expected:E,mitigated:h};function p(e,t){const n=e.resource;if(n===void 0)return!1;if("logicalIdContains"in n)return t.logicalId!==null&&t.logicalId.includes(n.logicalIdContains);if(t.tags===null)return!1;const i=t.tags[n.tag.key];return i!==void 0&&(n.tag.value===void 0||i===n.tag.value)}r(p,"scopeMatches");function O(e,t=l){const n=t.mitigated.filter(o=>o.ruleId===e.ruleId),i=t.expected.filter(o=>o.ruleId===e.ruleId),a=n.find(o=>p(o,e));if(a!==void 0)return{lane:"mitigated",row:a};const s=i.find(o=>p(o,e));if(s!==void 0)return{lane:"expected",row:s};const c=n.find(o=>o.resource===void 0);if(c!==void 0)return{lane:"mitigated",row:c};const u=i.find(o=>o.resource===void 0);return u===void 0?void 0:{lane:"expected",row:u}}r(O,"findPolicyRow");function S(e){return e===void 0?"":"logicalIdContains"in e?e.logicalIdContains:e.tag.value===void 0?`tag:${e.tag.key}`:`tag:${e.tag.key}=${e.tag.value}`}r(S,"scopeKey");function f(e){return`${e.lane}:${e.row.ruleId}:${S(e.row.resource)}`}r(f,"policyRowKey");function R(e,t=l){const n=t.expected.find(a=>f({lane:"expected",row:a})===e);if(n!==void 0)return{lane:"expected",row:n};const i=t.mitigated.find(a=>f({lane:"mitigated",row:a})===e);return i===void 0?void 0:{lane:"mitigated",row:i}}r(R,"findPolicyRowByKey");function A(e){return e.lane==="expected"?{cause:x(e.row),cure:_(e.row)}:{cause:e.row.name,evidence:w(e.row)}}r(A,"policyRowPresentation");function x(e){return`${e.knob.name} on ${e.knob.construct}`}r(x,"expectedRowCause");function _(e){return`Cure: set ${e.cure} in your infrastructure code.`}r(_,"cureClause");function w(e){return`Evidence: ${e.evidence}.`}r(w,"evidenceClause");function $(e,t){const n=I[e].heading;return t===null?n:`${n} \u2014 ${t}`}r($,"laneGroupHeading");export{l as DEFAULT_POLICY_REGISTRY,y as EXPECTED_LANE_STAGES,E as EXPECTED_POLICY,h as MITIGATED_POLICY,_ as cureClause,w as evidenceClause,x as expectedRowCause,O as findPolicyRow,R as findPolicyRowByKey,C as isExpectedLaneStage,$ as laneGroupHeading,f as policyRowKey,A as policyRowPresentation};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consent lines a create flow shows for a tier (alert policy design
|
|
3
|
+
* § 13.10): for the backup tier a chosen tier implies, the K1 rows the tier
|
|
4
|
+
* leaves at the default that produces the finding — so the create-app
|
|
5
|
+
* wizard and `fjall create` say, before the app exists, what the first
|
|
6
|
+
* posture scan will list and where. Derived from the registry and from the
|
|
7
|
+
* tier table the constructs read, so the wizard, the CLI, the scan and the
|
|
8
|
+
* deploy cannot disagree; the words are built here once so the two clients
|
|
9
|
+
* print them byte-identically. It informs the choice and gates nothing.
|
|
10
|
+
* Client-safe: the tier table is zod-only.
|
|
11
|
+
*/
|
|
12
|
+
import { type BackupTier } from "../backupTiers.js";
|
|
13
|
+
import { type ExpectedPolicyRow } from "./registry.js";
|
|
14
|
+
/**
|
|
15
|
+
* The K1 rows a backup tier leaves at the default that produces the finding,
|
|
16
|
+
* in registry order. An undefined tier reads the `standard` column, as the
|
|
17
|
+
* constructs do. Only rows keyed on the backup tier are lines: creating the
|
|
18
|
+
* app is what produces them. A row on another knob — the alarm topic's
|
|
19
|
+
* account-level `alarmTopicEncryption`, scoped to the Account's own
|
|
20
|
+
* construct — is produced by the account, not by the app being created, so
|
|
21
|
+
* it is not a line at any tier.
|
|
22
|
+
*/
|
|
23
|
+
export declare function expectedRowsAtDefault(backupTier: BackupTier | undefined, registry?: readonly ExpectedPolicyRow[]): ExpectedPolicyRow[];
|
|
24
|
+
/** One line of the note: the cause, the finding as it is listed, and the registry's own expectation words. */
|
|
25
|
+
export interface TierConsentLine {
|
|
26
|
+
/** `policyRowKey` of the expected row: the line's identity. */
|
|
27
|
+
rowKey: string;
|
|
28
|
+
ruleId: string;
|
|
29
|
+
/**
|
|
30
|
+
* The finding as the scan lists it: the rule's title where the caller
|
|
31
|
+
* holds the rule registry (the webapp wizard), else the rule id (the CLI).
|
|
32
|
+
*/
|
|
33
|
+
title: string;
|
|
34
|
+
/** The knob and the construct it sits on, as the asset page groups the finding. */
|
|
35
|
+
cause: string;
|
|
36
|
+
expectation: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function tierConsentLine(row: ExpectedPolicyRow, title: string): TierConsentLine;
|
|
39
|
+
/**
|
|
40
|
+
* Where the lines land, in the words the scan uses: the expected-lane stages
|
|
41
|
+
* and the status-label table's clauses, under the organisation's posture
|
|
42
|
+
* setting — the scan's ladder flags on a strict organisation before it
|
|
43
|
+
* looks at the stage, so a strict organisation is told `open` everywhere.
|
|
44
|
+
* `null` is a posture setting the caller could not read; the clause says so
|
|
45
|
+
* rather than promise either lane. Rendered once as the note's lead,
|
|
46
|
+
* because every K1 row shares the one lane rule.
|
|
47
|
+
*/
|
|
48
|
+
export declare function expectedLaneWhereClause(postureStrict: boolean | null): string;
|
|
49
|
+
/** The note's name: the wizard panel's accessible name and the CLI note's heading. */
|
|
50
|
+
export declare const TIER_CONSENT_NOTE_LABEL = "Posture findings this tier produces";
|
|
51
|
+
/**
|
|
52
|
+
* The note's lead in three parts, so a renderer may emphasise the tier name
|
|
53
|
+
* between them: "Choosing <tier> keeps these defaults, and the first posture
|
|
54
|
+
* scan lists them. <where the scan lists them> Nothing is accepted on your
|
|
55
|
+
* behalf." The where-clause is `expectedLaneWhereClause`.
|
|
56
|
+
*/
|
|
57
|
+
export interface TierConsentLead {
|
|
58
|
+
prefix: string;
|
|
59
|
+
tierName: string;
|
|
60
|
+
rest: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function tierConsentLead(tierName: string, postureStrict: boolean | null): TierConsentLead;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var i=Object.defineProperty;var s=(e,t)=>i(e,"name",{value:t,configurable:!0});import{tierDefault as a}from"../backupTiers.js";import{ISSUE_STATUS_LABELS as r}from"./issueStatusLabels.js";import{EXPECTED_LANE_STAGES as c,EXPECTED_POLICY as u,expectedRowCause as p,policyRowKey as l}from"./registry.js";function $(e,t=u){return t.filter(n=>n.knob.name==="backup.tier"&&n.tierDefault!==void 0&&a(e,n.tierDefault)===!1)}s($,"expectedRowsAtDefault");function g(e,t){return{rowKey:l({lane:"expected",row:e}),ruleId:e.ruleId,title:t,cause:p(e),expectation:e.expectation}}s(g,"tierConsentLine");function f(e){const t=e.slice(0,-1),n=e[e.length-1]??"";return t.length===0?n:`${t.join(", ")} or ${n}`}s(f,"orList");function d(e){const t=r.expected.clause,n=r.open.clause,o=f(c);return e===!0?`This organisation's posture is strict, so a posture scan lists each as ${n} on every account.`:e===null?`A posture scan lists each as ${t} on a ${o} account, or as ${n} everywhere if the organisation's posture is strict; the posture setting could not be read.`:`On a ${o} account a posture scan lists each as ${t}; on any other account it is ${n}.`}s(d,"expectedLaneWhereClause");const C="Posture findings this tier produces";function T(e,t){return{prefix:"Choosing ",tierName:e,rest:` keeps these defaults, and the first posture scan lists them. ${d(t)} Nothing is accepted on your behalf.`}}s(T,"tierConsentLead");export{C as TIER_CONSENT_NOTE_LABEL,d as expectedLaneWhereClause,$ as expectedRowsAtDefault,T as tierConsentLead,g as tierConsentLine};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/util",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "36.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
"types": "./dist/organisationBinding/index.d.ts",
|
|
64
64
|
"default": "./dist/organisationBinding/index.js"
|
|
65
65
|
},
|
|
66
|
+
"./posturePolicy": {
|
|
67
|
+
"types": "./dist/posturePolicy/index.d.ts",
|
|
68
|
+
"default": "./dist/posturePolicy/index.js"
|
|
69
|
+
},
|
|
66
70
|
"./environments": {
|
|
67
71
|
"types": "./dist/environments.d.ts",
|
|
68
72
|
"default": "./dist/environments.js"
|