filegrc 0.16.3 → 0.16.4
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/package.json +1 -1
- package/src/agent.js +2 -1
- package/src/audit-preparation.js +6 -1
- package/src/cli.js +69 -31
- package/src/index.js +1 -0
- package/src/program-path.js +141 -75
- package/src/program-readiness.js +95 -11
- package/src/state.js +1 -0
- package/src/web.js +52 -17
- package/src/workflow.js +1 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { modelSupports } from "../model/index.js";
|
|
2
2
|
import { createResourceId } from "./id.js";
|
|
3
3
|
import { markdownEntries } from "./resource-markdown.js";
|
|
4
|
-
import { RESOURCE_INSTRUCTIONS, resourceProgramContext } from "./program-path.js";
|
|
4
|
+
import { RESOURCE_INSTRUCTIONS, RESOURCE_OUTPUTS, resourceProgramContext } from "./program-path.js";
|
|
5
5
|
import { assessCollectionReview } from "./collection-review.js";
|
|
6
6
|
import { resolveProgram } from "./program.js";
|
|
7
7
|
|
|
@@ -80,6 +80,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
80
80
|
title: definition.title,
|
|
81
81
|
pluralTitle: definition.pluralTitle,
|
|
82
82
|
instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
|
|
83
|
+
output: RESOURCE_OUTPUTS[type] || `A validated ${definition.title} record.`,
|
|
83
84
|
use: definition.description,
|
|
84
85
|
purpose: definition.description,
|
|
85
86
|
programStep: resourceProgramContext(type, loaded.model),
|
package/src/audit-preparation.js
CHANGED
|
@@ -69,8 +69,13 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
69
69
|
? audits.find((record) => record.id === options.auditId)
|
|
70
70
|
: options.selectDefault === false
|
|
71
71
|
? null
|
|
72
|
-
: selectDefaultAudit(
|
|
72
|
+
: selectDefaultAudit(options.programId && modelSupports(loaded.model, "program-scope")
|
|
73
|
+
? audits.filter((record) => record.programId === options.programId)
|
|
74
|
+
: audits, calendarAsOf);
|
|
73
75
|
if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
|
|
76
|
+
if (audit && options.programId && modelSupports(loaded.model, "program-scope") && audit.programId !== options.programId) {
|
|
77
|
+
throw new Error(`Audit "${audit.id}" does not belong to Program "${options.programId}".`);
|
|
78
|
+
}
|
|
74
79
|
|
|
75
80
|
const formalPeriodEnd = audit?.auditKind === "soc-2-type-2" ? coverageEnd(audit.coverage) : null;
|
|
76
81
|
const readinessAsOf = formalPeriodEnd && formalPeriodEnd < calendarAsOf ? formalPeriodEnd : calendarAsOf;
|
package/src/cli.js
CHANGED
|
@@ -289,10 +289,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
289
289
|
const audit = auditId ? loaded.resources.find(({ id, type }) => id === auditId && type === "audit") : null;
|
|
290
290
|
const programId = flags.program || audit?.programId;
|
|
291
291
|
const readiness = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId });
|
|
292
|
-
const auditReadiness =
|
|
292
|
+
const auditReadiness = await assessAuditPreparation(loaded, {
|
|
293
293
|
auditId,
|
|
294
|
-
asOf: flags["as-of"]
|
|
295
|
-
|
|
294
|
+
asOf: flags["as-of"],
|
|
295
|
+
programId
|
|
296
|
+
});
|
|
296
297
|
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
297
298
|
const output = selectProgramPathOutput(result, flags);
|
|
298
299
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
@@ -458,15 +459,29 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
458
459
|
if (command === "program-readiness") {
|
|
459
460
|
const loaded = await loadWorkspace(root);
|
|
460
461
|
const result = await assessProgramReadiness(loaded, { asOf: flags["as-of"], programId: flags.program });
|
|
461
|
-
|
|
462
|
+
if (flags.control && flags.summary) throw new Error("Use either --control or --summary for program-readiness.");
|
|
463
|
+
const controlItem = flags.control
|
|
464
|
+
? result.stages.find(({ id }) => id === "controls")?.items.find(({ resourceType, resourceId }) => (
|
|
465
|
+
resourceType === "control" && resourceId === flags.control
|
|
466
|
+
))
|
|
467
|
+
: null;
|
|
468
|
+
if (flags.control && !controlItem) throw new Error(`Control "${flags.control}" is not selected in this Program.`);
|
|
469
|
+
const output = controlItem
|
|
470
|
+
? { asOf: result.asOf, programId: result.program?.id, ...controlItem }
|
|
471
|
+
: flags.summary ? summarizeProgramReadiness(result) : result;
|
|
462
472
|
const headlineStatus = result.progress.status || result.status;
|
|
463
473
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
474
|
+
else if (controlItem) {
|
|
475
|
+
console.log(`${controlItem.status.toUpperCase()}\t${controlItem.title}`);
|
|
476
|
+
if (!controlItem.nextSteps?.length) console.log(` ${controlItem.message}`);
|
|
477
|
+
printGuidanceSteps(controlItem);
|
|
478
|
+
}
|
|
464
479
|
else if (flags.summary) {
|
|
465
480
|
console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
|
|
466
481
|
for (const stage of output.stages) {
|
|
467
482
|
console.log(`${stage.status.toUpperCase()}\t${stage.title}\t${stage.counts.action} checks need action`);
|
|
468
483
|
}
|
|
469
|
-
if (output.firstAction)
|
|
484
|
+
if (output.firstAction) printNextAction(output.firstAction);
|
|
470
485
|
}
|
|
471
486
|
else {
|
|
472
487
|
console.log(`${headlineStatus.toUpperCase()}: ${result.progress.label} ${result.progress.percent}%. ${result.progress.detail}`);
|
|
@@ -480,7 +495,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
480
495
|
);
|
|
481
496
|
for (const stage of result.stages) {
|
|
482
497
|
console.log(`\n${stage.title}`);
|
|
483
|
-
for (const item of stage.items)
|
|
498
|
+
for (const item of stage.items) {
|
|
499
|
+
console.log(`${item.status.toUpperCase()}\t${item.title}${item.nextSteps?.length ? "" : `\t${item.message}`}`);
|
|
500
|
+
printGuidanceSteps(item);
|
|
501
|
+
}
|
|
484
502
|
}
|
|
485
503
|
if (result.policyActivations.length) {
|
|
486
504
|
console.log("\nPolicy activation assessments");
|
|
@@ -492,7 +510,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
492
510
|
console.log(`\nEvidence Ready: management can start the candidate Type 2 period on or after ${result.suggestedCandidatePeriodStart || result.asOf}.`);
|
|
493
511
|
}
|
|
494
512
|
}
|
|
495
|
-
if (flags["require-ready"] && !result.evidenceReady) process.exitCode = 2;
|
|
513
|
+
if (flags["require-ready"] && (controlItem ? controlItem.status !== "complete" : !result.evidenceReady)) process.exitCode = 2;
|
|
496
514
|
return output;
|
|
497
515
|
}
|
|
498
516
|
if (command === "reporting-route-sets") {
|
|
@@ -1347,7 +1365,7 @@ Usage:
|
|
|
1347
1365
|
filegrc list [resource-type] [--workflow] [--json]
|
|
1348
1366
|
filegrc search <query> [--type resource-type] [--json]
|
|
1349
1367
|
filegrc obligations [--program program-id] [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
1350
|
-
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
1368
|
+
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary | --control CONTROL_ID] [--json]
|
|
1351
1369
|
filegrc program-amendment <source-resource-id> [--json]
|
|
1352
1370
|
filegrc review-bindings <retention-or-mapping-id> [--json]
|
|
1353
1371
|
filegrc evidence-map [--as-of YYYY-MM-DD] [--json]
|
|
@@ -1365,7 +1383,7 @@ Usage:
|
|
|
1365
1383
|
filegrc policy-library [--json | --accept proposal-id --proposal-revision revision --yes]
|
|
1366
1384
|
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--program program-id] [--risk-level normal|high] [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
1367
1385
|
filegrc evidence-packet [--audit audit-id | --program program-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
|
|
1368
|
-
filegrc get [resource-type] <id> [--mutation]
|
|
1386
|
+
filegrc get [resource-type] <id> [--mutation|--workflow]
|
|
1369
1387
|
filegrc references <id> [--json]
|
|
1370
1388
|
filegrc preview-mutation <preview.json|-> [--json]
|
|
1371
1389
|
filegrc create <mutation.json|-> [--json]
|
|
@@ -1496,8 +1514,9 @@ source. No audit ID or CPA firm is required.
|
|
|
1496
1514
|
Options:
|
|
1497
1515
|
--program <id> Program to assess when more than one active Program exists
|
|
1498
1516
|
--as-of <date> Evaluate effective dates and obligations on YYYY-MM-DD
|
|
1499
|
-
--require-ready Exit with code 2 unless the
|
|
1517
|
+
--require-ready Exit with code 2 unless the selected Control, or the full program, is ready
|
|
1500
1518
|
--summary Omit item details and print stage counts and next actions
|
|
1519
|
+
--control <id> Show the current setup steps for one selected Control
|
|
1501
1520
|
--json Print the result as JSON
|
|
1502
1521
|
--root <path> Workspace path
|
|
1503
1522
|
--help Show this help`);
|
|
@@ -1749,6 +1768,7 @@ function printAgentGuide(result) {
|
|
|
1749
1768
|
console.log(`Program step: ${result.programStep.order ? `Step ${result.programStep.order}` : `Step ${result.programStep.number}`} · ${result.programStep.title}`);
|
|
1750
1769
|
}
|
|
1751
1770
|
console.log(`Instructions: ${result.instructions}`);
|
|
1771
|
+
console.log(`Output: ${result.output}`);
|
|
1752
1772
|
console.log(`Use: ${result.use}`);
|
|
1753
1773
|
console.log(`Policy basis: ${result.policyBasis}`);
|
|
1754
1774
|
console.log(`Timing: ${result.cadence}`);
|
|
@@ -1810,23 +1830,21 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1810
1830
|
const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
|
|
1811
1831
|
const stages = buildAgentProgramPath(model).map((stage) => {
|
|
1812
1832
|
if (stage.id === "audit") {
|
|
1813
|
-
const auditAction = auditReadiness?.
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
: null
|
|
1829
|
-
);
|
|
1833
|
+
const auditAction = auditReadiness?.status === "not-started"
|
|
1834
|
+
? {
|
|
1835
|
+
id: "create-audit",
|
|
1836
|
+
status: "action",
|
|
1837
|
+
title: "Record the CPA engagement",
|
|
1838
|
+
message: "After engaging the CPA firm, create an Audit with the firm-agreed type, scope, and dates.",
|
|
1839
|
+
resourceType: "audit",
|
|
1840
|
+
commands: [
|
|
1841
|
+
"npx filegrc guide audit --json",
|
|
1842
|
+
"npx filegrc scaffold audit --title \"YEAR SOC 2 TYPE\" > audit-mutation.json",
|
|
1843
|
+
"npx filegrc create audit-mutation.json --json",
|
|
1844
|
+
"npx filegrc prepare-audit AUDIT_ID --json"
|
|
1845
|
+
]
|
|
1846
|
+
}
|
|
1847
|
+
: auditReadiness?.stages?.flatMap(({ items }) => items).find(({ status }) => status === "action") || null;
|
|
1830
1848
|
return {
|
|
1831
1849
|
...stage,
|
|
1832
1850
|
status: auditReadiness?.status || "not-started",
|
|
@@ -1866,12 +1884,29 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1866
1884
|
};
|
|
1867
1885
|
}
|
|
1868
1886
|
|
|
1887
|
+
function printGuidanceSteps(item) {
|
|
1888
|
+
item.nextSteps?.forEach((step, index) => console.log(` ${index + 1}. ${step}`));
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function printNextAction(action) {
|
|
1892
|
+
if (action.nextSteps?.length) {
|
|
1893
|
+
console.log(`Next: ${action.title}`);
|
|
1894
|
+
printGuidanceSteps(action);
|
|
1895
|
+
} else {
|
|
1896
|
+
console.log(`Next: ${action.title} · ${action.message}`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1869
1900
|
function printProgramPath(result) {
|
|
1870
1901
|
console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
|
|
1871
1902
|
console.log(`Evidence Ready: ${result.evidenceReady ? "yes" : "no"}; operating: ${result.operating ? "yes" : "no"}`);
|
|
1872
1903
|
for (const stage of result.stages) {
|
|
1873
1904
|
console.log(`\nStep ${stage.number}. ${stage.title} [${String(stage.status).toUpperCase()}]`);
|
|
1874
1905
|
console.log(stage.summary);
|
|
1906
|
+
for (const section of stage.sections) {
|
|
1907
|
+
console.log(` ${section.title}`);
|
|
1908
|
+
section.steps.forEach((step, index) => console.log(` ${index + 1}. ${step}`));
|
|
1909
|
+
}
|
|
1875
1910
|
for (const page of stage.pages) {
|
|
1876
1911
|
console.log(`${page.order ? `Step ${page.order}` : "Operating area"} · ${page.title} (${page.type || `utility:${page.utility}`})`);
|
|
1877
1912
|
console.log(` ${page.summary}`);
|
|
@@ -1885,7 +1920,7 @@ function printProgramPath(result) {
|
|
|
1885
1920
|
}
|
|
1886
1921
|
console.log("Commands:");
|
|
1887
1922
|
for (const command of stage.commands) console.log(` ${command}`);
|
|
1888
|
-
for (const action of stage.nextActions)
|
|
1923
|
+
for (const action of stage.nextActions) printNextAction(action);
|
|
1889
1924
|
}
|
|
1890
1925
|
}
|
|
1891
1926
|
|
|
@@ -1953,6 +1988,7 @@ function summarizePathAction(action) {
|
|
|
1953
1988
|
status: action.status,
|
|
1954
1989
|
title: action.title,
|
|
1955
1990
|
message: action.message,
|
|
1991
|
+
...(action.nextSteps?.length ? { nextSteps: action.nextSteps } : {}),
|
|
1956
1992
|
...(action.resourceType ? { resourceType: action.resourceType } : {}),
|
|
1957
1993
|
...(action.resourceId ? { resourceId: action.resourceId } : {})
|
|
1958
1994
|
};
|
|
@@ -1998,13 +2034,13 @@ function printProgramPathOutput(result, flags) {
|
|
|
1998
2034
|
console.log(`${String(stage.status).toUpperCase()}\tStep ${stage.number}\t${stage.title}`);
|
|
1999
2035
|
}
|
|
2000
2036
|
const current = result.stages.find(({ id }) => id === result.currentStep.id);
|
|
2001
|
-
if (current?.nextAction)
|
|
2037
|
+
if (current?.nextAction) printNextAction(current.nextAction);
|
|
2002
2038
|
return;
|
|
2003
2039
|
}
|
|
2004
2040
|
if (flags.next) {
|
|
2005
2041
|
console.log(`Current: Step ${result.currentStep.number}, ${result.currentStep.title}`);
|
|
2006
2042
|
if (result.step?.nextAction) {
|
|
2007
|
-
|
|
2043
|
+
printNextAction(result.step.nextAction);
|
|
2008
2044
|
}
|
|
2009
2045
|
for (const command of result.step?.commands || []) console.log(` ${command}`);
|
|
2010
2046
|
return;
|
|
@@ -2020,7 +2056,8 @@ function printWorkflow(result) {
|
|
|
2020
2056
|
console.log(`\n${result.counts.findings.ready || 0} ready findings, ${result.counts.workItems.overdue || 0} overdue Work Items`);
|
|
2021
2057
|
if (result.recommended) {
|
|
2022
2058
|
console.log(`Next: ${result.recommended.title}`);
|
|
2023
|
-
if (result.recommended.message) console.log(` ${result.recommended.message}`);
|
|
2059
|
+
if (!result.recommended.nextSteps?.length && result.recommended.message) console.log(` ${result.recommended.message}`);
|
|
2060
|
+
printGuidanceSteps(result.recommended);
|
|
2024
2061
|
const command = result.recommended.nextAction?.command || result.recommended.actions?.[0]?.command;
|
|
2025
2062
|
if (command) console.log(` ${command}`);
|
|
2026
2063
|
}
|
|
@@ -2035,6 +2072,7 @@ function summarizeProgramReadiness(result) {
|
|
|
2035
2072
|
status: item.status,
|
|
2036
2073
|
title: item.title,
|
|
2037
2074
|
...(options.message === false ? {} : { message: item.message }),
|
|
2075
|
+
...(item.nextSteps?.length ? { nextSteps: item.nextSteps } : {}),
|
|
2038
2076
|
...(item.resourceType ? { resourceType: item.resourceType } : {}),
|
|
2039
2077
|
...(item.resourceId ? { resourceId: item.resourceId } : {})
|
|
2040
2078
|
} : null;
|
package/src/index.js
CHANGED
package/src/program-path.js
CHANGED
|
@@ -1,53 +1,103 @@
|
|
|
1
1
|
import { ACTIVE_MODEL_VERSION, modelSupports } from "../model/index.js";
|
|
2
2
|
|
|
3
3
|
export const RESOURCE_INSTRUCTIONS = {
|
|
4
|
-
program: "
|
|
5
|
-
person: "Record
|
|
6
|
-
appointment: "
|
|
7
|
-
team: "
|
|
8
|
-
system: "
|
|
9
|
-
component: "
|
|
10
|
-
vendor: "
|
|
11
|
-
classification: "
|
|
12
|
-
"information-type": "
|
|
13
|
-
framework: "
|
|
14
|
-
requirement: "
|
|
15
|
-
commitment: "Record
|
|
16
|
-
"requirement-mapping": "
|
|
17
|
-
"reporting-route-set": "
|
|
18
|
-
policy: "
|
|
19
|
-
document: "Complete
|
|
20
|
-
control: "
|
|
21
|
-
"complementary-control": "
|
|
22
|
-
evidence: "
|
|
23
|
-
"risk-assessment": "
|
|
24
|
-
risk: "
|
|
25
|
-
obligation: "
|
|
26
|
-
"retention-schedule-item": "
|
|
27
|
-
"obligation-event": "
|
|
28
|
-
"policy-review": "Record
|
|
29
|
-
meeting: "Record
|
|
30
|
-
exception: "Record
|
|
31
|
-
asset: "
|
|
32
|
-
"vendor-review": "
|
|
33
|
-
"access-grant": "Record
|
|
34
|
-
"access-review": "Review access
|
|
35
|
-
"service-account": "
|
|
36
|
-
training: "
|
|
37
|
-
attestation: "Record
|
|
38
|
-
"vulnerability-scan": "Record
|
|
39
|
-
vulnerability: "Track confirmed
|
|
40
|
-
"penetration-test": "Record
|
|
41
|
-
incident: "Record
|
|
42
|
-
"backup-test": "Record
|
|
43
|
-
exercise: "Record
|
|
44
|
-
finding: "
|
|
45
|
-
"action-item": "
|
|
46
|
-
audit: "
|
|
47
|
-
"audit-request": "
|
|
48
|
-
"data-request": "Record
|
|
49
|
-
"control-test": "Record a management
|
|
50
|
-
"audit-population": "Record
|
|
4
|
+
program: "Set the Program goal, owners, Systems, Frameworks, criteria decisions, and risk method.",
|
|
5
|
+
person: "Record the person’s actual job title and current status.",
|
|
6
|
+
appointment: "Assign a named authority to one Person with its start date and scope.",
|
|
7
|
+
team: "Confirm the Team’s members and chair.",
|
|
8
|
+
system: "Define the service boundary, exclusions, owners, and information it handles.",
|
|
9
|
+
component: "Record a material service or evidence source and its role in each linked System.",
|
|
10
|
+
vendor: "Record each material external provider and the Components it supplies.",
|
|
11
|
+
classification: "Confirm the handling categories and apply them to information and evidence.",
|
|
12
|
+
"information-type": "Name a stable category of information, set its default Classification, and link its uses.",
|
|
13
|
+
framework: "Select the criteria Framework and version used by the Program.",
|
|
14
|
+
requirement: "Review whether this criterion applies to the Program and record the decision and rationale.",
|
|
15
|
+
commitment: "Record a real customer or service promise and link the Systems and Controls that fulfill it.",
|
|
16
|
+
"requirement-mapping": "Compare the supplemental source with the Requirement or Control, record the relationship and rationale, then review the source revisions.",
|
|
17
|
+
"reporting-route-set": "Replace the reporting-route placeholders with real normal and fallback channels, name the responsible role, and commit the proposal.",
|
|
18
|
+
policy: "Replace Policy placeholders with actual commitments, then have a separate approver approve the exact Markdown revision.",
|
|
19
|
+
document: "Complete the Document Markdown and intended values, assign an owner, and have a separate approver approve the exact revision.",
|
|
20
|
+
control: "Open this Control’s current next steps. Set up the activity, record how it works, and mark it Implemented when it works.",
|
|
21
|
+
"complementary-control": "Record each real customer or carved-out provider dependency, or confirm that the current scope has none.",
|
|
22
|
+
evidence: "Record a real artifact or approved reference, link its source and supported work, then have another person verify it.",
|
|
23
|
+
"risk-assessment": "Assess the in-scope service, record conclusions, and obtain an independent review.",
|
|
24
|
+
risk: "Name the threat and impact, assign an owner, rate the risk, and record its response.",
|
|
25
|
+
obligation: "Confirm the owner, cadence or trigger, deadline, and proof, then enable the rule when its Control is ready.",
|
|
26
|
+
"retention-schedule-item": "Set the Information Types, scope, cutoff, period, disposition, and sources for one retention rule.",
|
|
27
|
+
"obligation-event": "Record the actual event date and subject, then complete its linked Work Queue actions.",
|
|
28
|
+
"policy-review": "Record the review date, reviewer, decision, and any required follow-up.",
|
|
29
|
+
meeting: "Record attendees, decisions, minutes, and assigned follow-up for the meeting.",
|
|
30
|
+
exception: "Record the scope, owner, approval, and expiry before the departure begins.",
|
|
31
|
+
asset: "Record an important Asset’s owner, custody, location, and status.",
|
|
32
|
+
"vendor-review": "Review one Vendor’s current risk and evidence, then record the decision and follow-up.",
|
|
33
|
+
"access-grant": "Record the approved privilege, recipient, Component, and provisioning or removal dates.",
|
|
34
|
+
"access-review": "Review the current access list, record each decision, and assign the required changes.",
|
|
35
|
+
"service-account": "Record a non-human account’s owner, purpose, System, privilege, and expiry.",
|
|
36
|
+
training: "Complete the content and audience, then have a separate approver approve the exact revision.",
|
|
37
|
+
attestation: "Record the person’s dated acknowledgement of the exact active content revision.",
|
|
38
|
+
"vulnerability-scan": "Record the scan scope, date, result, and supporting evidence.",
|
|
39
|
+
vulnerability: "Track a confirmed weakness through its owner, response, and closure proof.",
|
|
40
|
+
"penetration-test": "Record the test provider, scope, period, results, evidence, and follow-up.",
|
|
41
|
+
incident: "Record the event, response decisions, dates, evidence, and follow-up.",
|
|
42
|
+
"backup-test": "Record the restore target, date, result, evidence, and follow-up.",
|
|
43
|
+
exercise: "Record the scenario, participants, date, result, and follow-up.",
|
|
44
|
+
finding: "Record a confirmed gap that needs its own owner, due date, remediation, and verified closure.",
|
|
45
|
+
"action-item": "Assign separate follow-up with an owner, deadline, source record, and completion proof.",
|
|
46
|
+
audit: "After engaging the CPA firm, record the agreed type, scope, Systems, criteria, and period.",
|
|
47
|
+
"audit-request": "Record the auditor’s request, owner, due date, and approved response.",
|
|
48
|
+
"data-request": "Record the request scope, decision, response, and supporting evidence.",
|
|
49
|
+
"control-test": "Record a management test only when it was performed and reviewed; link its result and evidence.",
|
|
50
|
+
"audit-population": "Record the complete Type 2 population, source query, count, fixed export, and reconciliation."
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const RESOURCE_OUTPUTS = {
|
|
54
|
+
program: "A Program record with the selected goal, scope, owners, criteria decisions, and risk method.",
|
|
55
|
+
person: "A Person record with their actual job title and current status.",
|
|
56
|
+
appointment: "A dated Appointment that names the holder, authority, and scope.",
|
|
57
|
+
team: "A Team record with its current members and chair.",
|
|
58
|
+
system: "A bounded System record with its services, data, owners, and exclusions.",
|
|
59
|
+
component: "A Component record linked to each System it supports, with a role and rationale.",
|
|
60
|
+
vendor: "A Vendor record for each material provider relationship.",
|
|
61
|
+
classification: "Reviewed handling categories used by the program's records.",
|
|
62
|
+
"information-type": "An Information Type with its default Classification and actual uses.",
|
|
63
|
+
framework: "The Framework and version selected for the Program.",
|
|
64
|
+
requirement: "A reviewed applicability decision on the Program for each selected Requirement.",
|
|
65
|
+
commitment: "A Commitment linked to the Systems and Controls that fulfill it.",
|
|
66
|
+
"requirement-mapping": "A reviewed mapping with its comparison, rationale, and source revisions.",
|
|
67
|
+
"reporting-route-set": "A proposed set of normal and fallback reporting routes with an owner and approval path.",
|
|
68
|
+
policy: "An independently approved Policy bound to its exact Markdown revision.",
|
|
69
|
+
document: "A governed Document with reviewed Markdown, an owner, and a separate approver.",
|
|
70
|
+
control: "An implemented Control with its actual procedure, scope, owner, source, and start date.",
|
|
71
|
+
"complementary-control": "A recorded customer or provider dependency, or a current review confirming none.",
|
|
72
|
+
evidence: "A verified Evidence Artifact linked to its source and supported work.",
|
|
73
|
+
"risk-assessment": "A completed, independently reviewed Risk Assessment with its conclusions.",
|
|
74
|
+
risk: "A rated Risk with an owner, response, and linked treatment Controls.",
|
|
75
|
+
obligation: "An enabled schedule or event rule with an owner, deadline, and required proof.",
|
|
76
|
+
"retention-schedule-item": "A reviewed retention row with its scope, cutoff, period, disposition, and sources.",
|
|
77
|
+
"obligation-event": "A dated Policy Event with its linked Work Queue actions.",
|
|
78
|
+
"policy-review": "A dated review with its decision, reviewer, and follow-up.",
|
|
79
|
+
meeting: "Meeting minutes with attendees, decisions, and assigned follow-up.",
|
|
80
|
+
exception: "A time-limited Exception with approval, scope, and expiry.",
|
|
81
|
+
asset: "An inventory record with the Asset's owner, custody, and status.",
|
|
82
|
+
"vendor-review": "A dated Vendor Review with a decision, evidence, and follow-up.",
|
|
83
|
+
"access-grant": "A current Access Grant showing approval, privilege, and lifecycle dates.",
|
|
84
|
+
"access-review": "A dated Access Review with decisions and assigned changes.",
|
|
85
|
+
"service-account": "A Service Account record with its owner, purpose, privilege, and expiry.",
|
|
86
|
+
training: "Approved Training content bound to its exact revision.",
|
|
87
|
+
attestation: "A person's dated acknowledgement bound to the exact content revision.",
|
|
88
|
+
"vulnerability-scan": "A dated scan record with scope, results, and linked evidence.",
|
|
89
|
+
vulnerability: "A tracked weakness with its owner, response, and closure proof.",
|
|
90
|
+
"penetration-test": "A test record with scope, provider, results, and follow-up.",
|
|
91
|
+
incident: "An Incident record with response decisions, dates, evidence, and follow-up.",
|
|
92
|
+
"backup-test": "A dated restore test with its result, evidence, and follow-up.",
|
|
93
|
+
exercise: "A dated exercise with participants, result, and follow-up.",
|
|
94
|
+
finding: "A confirmed Finding with an owner, due date, remediation, and verified closure.",
|
|
95
|
+
"action-item": "An assigned Action Item with a deadline and completion proof.",
|
|
96
|
+
audit: "An Audit record with the CPA-agreed type, scope, and period.",
|
|
97
|
+
"audit-request": "An owned Audit Request with a due date and approved response.",
|
|
98
|
+
"data-request": "A Data Request record with its scope, decision, and response.",
|
|
99
|
+
"control-test": "A reviewed management Control Test with results and linked evidence.",
|
|
100
|
+
"audit-population": "A reconciled Type 2 population linked to its fixed source export."
|
|
51
101
|
};
|
|
52
102
|
|
|
53
103
|
export const RESOURCE_PAGE_SUMMARIES = {
|
|
@@ -68,7 +118,7 @@ export const RESOURCE_PAGE_SUMMARIES = {
|
|
|
68
118
|
"retention-schedule-item": "Review each structured retention rule.",
|
|
69
119
|
policy: "Tailor the starter Policy and have someone other than its owner approve it.",
|
|
70
120
|
document: "Adapt and approve plans.",
|
|
71
|
-
control: "
|
|
121
|
+
control: "Put each Control in place and record how it works.",
|
|
72
122
|
"complementary-control": "Record customer or provider responsibilities, or confirm there are none.",
|
|
73
123
|
audit: "Record the CPA engagement and scope.",
|
|
74
124
|
"audit-request": "Track fieldwork requests and responses.",
|
|
@@ -82,11 +132,11 @@ export const PROGRAM_PATH = [
|
|
|
82
132
|
number: 1,
|
|
83
133
|
title: "Define Scope",
|
|
84
134
|
description: "Ownership, criteria, and service boundary",
|
|
85
|
-
summary: "
|
|
135
|
+
summary: "A reviewed Program scope with owners, criteria, Systems, and material providers.",
|
|
86
136
|
sections: [
|
|
87
|
-
{ id: "ownership", title: "Program Ownership", description: "Confirm who owns the program, plus the normal security reporting channel and its fallback.", steps: ["Confirm the
|
|
88
|
-
{ id: "criteria", title: "Program and Criteria", description: "Define the Program, confirm its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["
|
|
89
|
-
{ id: "boundary", title: "System Boundary", description: "Start with the bounded System. Add Components that materially deliver the service, support Controls, produce authoritative Evidence, or support relevant operations. Keep Vendor relationships and specific Assets separate.", steps: ["
|
|
137
|
+
{ id: "ownership", title: "Program Ownership", description: "Confirm who owns the program, plus the normal security reporting channel and its fallback.", steps: ["Confirm the program lead, policy owner, independent reviewer, and oversight team.", "Replace the reporting-channel placeholders with real normal and fallback routes, then commit the proposal."], types: ["person", "appointment", "team", "reporting-route-set"], defaultOpen: true },
|
|
138
|
+
{ id: "criteria", title: "Program and Criteria", description: "Define the Program, confirm its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["Set the Program goal, owners, risk method, and tentative timing.", "Review each selected criterion for this service and record its applicability decision.", "Replace the starter Commitment prompt with the real promise; add only needed supplemental mappings."], types: ["program", "framework", "requirement", "commitment", "requirement-mapping"], defaultOpen: true },
|
|
139
|
+
{ id: "boundary", title: "System Boundary", description: "Start with the bounded System. Add Components that materially deliver the service, support Controls, produce authoritative Evidence, or support relevant operations. Keep Vendor relationships and specific Assets separate.", steps: ["Define the service and exclusions in a bounded System selected by the Program.", "Add material Components and Vendors with their actual System relationships.", "Record the Information Types and Classifications the service uses."], types: ["system", "component", "vendor", "classification", "information-type"], defaultOpen: false }
|
|
90
140
|
],
|
|
91
141
|
resourceTypes: ["person", "appointment", "team", "reporting-route-set", "program", "framework", "requirement", "commitment", "requirement-mapping", "system", "component", "vendor", "classification", "information-type"],
|
|
92
142
|
commands: [
|
|
@@ -108,10 +158,10 @@ export const PROGRAM_PATH = [
|
|
|
108
158
|
number: 2,
|
|
109
159
|
title: "Approve Policies",
|
|
110
160
|
description: "Approve governed content and retention decisions",
|
|
111
|
-
summary: "
|
|
161
|
+
summary: "Approved Policies, program Documents, Training content, and retention decisions.",
|
|
112
162
|
sections: [
|
|
113
|
-
{ id: "policy-content", title: "Policies", description: "
|
|
114
|
-
{ id: "retention", title: "Data Retention Schedule", description: "
|
|
163
|
+
{ id: "policy-content", title: "Policies", description: "Approve the program's governing content.", instructions: "Open each Policy, program Document, and Training record. Replace placeholders, confirm the owner and intended values, then have a separate person approve the exact Markdown revision.", output: "Approved content bound to the reviewed revisions.", steps: ["Replace placeholders with the company's real rules, plans, and training material.", "Confirm the owner, separate approver, linked Controls, audience, and intended values.", "Record the independent approval and date against each exact Markdown revision."], types: [], relatedLinks: [{ type: "policy", label: "Policies", href: "#/policies" }], defaultOpen: true },
|
|
164
|
+
{ id: "retention", title: "Data Retention Schedule", description: "Approve the schedule document and its completed rows together.", steps: ["Review the governing schedule and any legal hold or exception rules.", "Complete a row for each distinct information use, cutoff, retention period, and disposition.", "Have a separate reviewer approve the complete document and row set."], types: [], utility: "retention-schedule", defaultOpen: true }
|
|
115
165
|
],
|
|
116
166
|
resourceTypes: [],
|
|
117
167
|
supportingResourceTypes: ["policy", "document", "training", "retention-schedule-item"],
|
|
@@ -132,12 +182,12 @@ export const PROGRAM_PATH = [
|
|
|
132
182
|
number: 3,
|
|
133
183
|
title: "Implement Controls",
|
|
134
184
|
description: "Implement and prepare to operate",
|
|
135
|
-
summary: "
|
|
185
|
+
summary: "Implemented Controls with ready evidence sources and enabled work schedules.",
|
|
136
186
|
sections: [
|
|
137
|
-
{ id: "controls", title: "Controls", description: "
|
|
138
|
-
{ id: "complementary-controls", title: "Complementary Controls", description: "Record customer or provider responsibilities that form part of the implementation boundary, or confirm there are none.", steps: ["
|
|
139
|
-
{ id: "evidence-sources", title: "Evidence Sources", description: "Check that every Control points to an authoritative source that can produce its expected evidence.", steps: ["
|
|
140
|
-
{ id: "obligations", title: "Obligations", description: "Configure the calendar and event schedules that operate the Controls.", steps: ["
|
|
187
|
+
{ id: "controls", title: "Controls", description: "Put each selected Control into use and record what is actually in place.", steps: ["Open each Control for its specific setup action and current missing checks.", "Configure the activity and record its procedure, Systems, owner, and evidence sources.", "Mark it Implemented with its real start date after its sources and schedules are ready."], types: ["control"], defaultOpen: true },
|
|
188
|
+
{ id: "complementary-controls", title: "Complementary Controls", description: "Record customer or provider responsibilities that form part of the implementation boundary, or confirm there are none.", steps: ["Identify any customer or carved-out provider action an in-scope Control depends on.", "Record each dependency, or confirm that the current scope has none."], types: ["complementary-control"], defaultOpen: true },
|
|
189
|
+
{ id: "evidence-sources", title: "Evidence Sources", description: "Check that every Control points to an authoritative source that can produce its expected evidence.", steps: ["For each Control family, link an active Component with the required source kind, access owners, and retrieval instructions."], types: [], utility: "evidence-sources", defaultOpen: true },
|
|
190
|
+
{ id: "obligations", title: "Obligations", description: "Configure the calendar and event schedules that operate the Controls.", steps: ["Check the owner, trigger or cadence, due window, and required proof for each Obligation.", "Enable the rules that will run the implemented Controls."], types: ["obligation"], defaultOpen: true }
|
|
141
191
|
],
|
|
142
192
|
resourceTypes: ["control", "complementary-control", "obligation"],
|
|
143
193
|
commands: [
|
|
@@ -160,17 +210,17 @@ export const PROGRAM_PATH = [
|
|
|
160
210
|
number: 4,
|
|
161
211
|
title: "Operate the Program",
|
|
162
212
|
description: "Run the work and retain dated proof",
|
|
163
|
-
summary: "
|
|
213
|
+
summary: "Dated operating records and linked proof for work actually performed.",
|
|
164
214
|
sections: [
|
|
165
|
-
{ id: "risk", title: "Risk", description: "Maintain the program’s risk assessments and risk register as the service, threats, suppliers, and control needs change.", steps: ["Complete and approve
|
|
166
|
-
{ id: "queue", title: "Work Queue", description: "Complete recurring occurrences, Policy Event tasks, and assigned follow-up within their required windows.", steps: ["Complete due
|
|
167
|
-
{ id: "evidence", title: "Evidence Artifacts", description: "Create records only for real exports, reports, screenshots, signed files, or approved external references collected during operation.", steps: ["Create an Evidence Artifact when
|
|
168
|
-
{ id: "governance", title: "Governance", description: "Record formal reviews, oversight meetings, and approved policy or control exceptions.", steps: ["Complete scheduled policy reviews and oversight meetings.", "
|
|
169
|
-
{ id: "inventories", title: "Assets and Vendors", description: "Maintain the asset inventory and recurring reviews of supplier relationships during operation.", steps: ["Keep
|
|
170
|
-
{ id: "access-training", title: "Access and Training Completion", description: "Inventory service accounts and retain access decisions, Training assignments, and acknowledgements produced during operation.", steps: ["Catalog
|
|
171
|
-
{ id: "security", title: "Security Operations", description: "Record vulnerability work, applicable penetration testing, and incident response activity for the period.", steps: ["
|
|
172
|
-
{ id: "resilience", title: "Resilience", description: "Preserve proof that backups, restoration, continuity, and incident exercises work as designed.", steps: ["
|
|
173
|
-
{ id: "issues", title: "Issues and Remediation", description: "Keep observations in the source report and track only confirmed gaps that need a separate remediation lifecycle.", steps: ["Create a Finding
|
|
215
|
+
{ id: "risk", title: "Risk", description: "Maintain the program’s risk assessments and risk register as the service, threats, suppliers, and control needs change.", steps: ["Complete and approve the required Risk Assessments.", "Record each risk that needs a response, owner, and treatment Controls."], types: ["risk-assessment", "risk"], defaultOpen: true },
|
|
216
|
+
{ id: "queue", title: "Work Queue", description: "Complete recurring occurrences, Policy Event tasks, and assigned follow-up within their required windows.", steps: ["Complete due Work Queue occurrences with dated proof.", "Trigger a Policy Event when its real-world event occurs, then complete the linked Action Items."], types: ["obligation-event", "data-request"], utility: "obligation-board", defaultOpen: true },
|
|
217
|
+
{ id: "evidence", title: "Evidence Artifacts", description: "Create records only for real exports, reports, screenshots, signed files, or approved external references collected during operation.", steps: ["Create an Evidence Artifact only when a real file or approved external reference exists.", "Link its source Component, supported Controls and work record, and fixed artifact or reference.", "Record the collector and Classification, then verify the artifact."], types: ["evidence"], defaultOpen: true },
|
|
218
|
+
{ id: "governance", title: "Governance", description: "Record formal reviews, oversight meetings, and approved policy or control exceptions.", steps: ["Complete scheduled policy reviews and oversight meetings with decisions and evidence.", "Create separate Action Items only for follow-up with its own owner and deadline.", "Approve any time-limited Exception before the departure begins."], types: ["policy-review", "meeting", "exception"], defaultOpen: false },
|
|
219
|
+
{ id: "inventories", title: "Assets and Vendors", description: "Maintain the asset inventory and recurring reviews of supplier relationships during operation.", steps: ["Keep important Assets and Vendor relationships current.", "Complete Vendor Reviews on schedule or after material changes, with a decision and evidence."], types: ["asset", "vendor-review"], defaultOpen: false },
|
|
220
|
+
{ id: "access-training", title: "Access and Training Completion", description: "Inventory service accounts and retain access decisions, Training assignments, and acknowledgements produced during operation.", steps: ["Catalog Service Accounts that need separate tracking.", "Record access decisions and removals as they occur; complete scheduled Access Reviews.", "Keep Training assignments and Attestations tied to the exact active content revision."], types: ["service-account", "access-grant", "access-review", "attestation"], defaultOpen: false },
|
|
221
|
+
{ id: "security", title: "Security Operations", description: "Record vulnerability work, applicable penetration testing, and incident response activity for the period.", steps: ["Record required scans and tests with their real scope, results, and evidence.", "Track confirmed weaknesses and their remediation.", "Start the Incident workflow when a qualifying event occurs."], types: ["vulnerability-scan", "vulnerability", "penetration-test", "incident"], defaultOpen: false },
|
|
222
|
+
{ id: "resilience", title: "Resilience", description: "Preserve proof that backups, restoration, continuity, and incident exercises work as designed.", steps: ["Run scheduled restore tests and continuity or incident exercises.", "Record the result and evidence, then assign follow-up for failed objectives."], types: ["backup-test", "exercise"], defaultOpen: false },
|
|
223
|
+
{ id: "issues", title: "Issues and Remediation", description: "Keep observations in the source report and track only confirmed gaps that need a separate remediation lifecycle.", steps: ["Create a Finding for a confirmed gap that needs a separate owner, deadline, or closure review.", "Use Action Items only for separately assigned tasks.", "Close the Finding after remediation is independently verified."], types: ["finding"], defaultOpen: false }
|
|
174
224
|
],
|
|
175
225
|
resourceTypes: [
|
|
176
226
|
"risk-assessment",
|
|
@@ -238,10 +288,10 @@ export const PROGRAM_PATH = [
|
|
|
238
288
|
number: 5,
|
|
239
289
|
title: "Audit",
|
|
240
290
|
description: "Firm, formal period, fieldwork, and report",
|
|
241
|
-
summary: "
|
|
291
|
+
summary: "A CPA engagement record and a reviewable, period-bound evidence packet.",
|
|
242
292
|
sections: [
|
|
243
|
-
{ id: "engagement", title: "Engagement", description: "Record the actual CPA engagement, formal scope and dates, requests, and management responses.", steps: ["
|
|
244
|
-
{ id: "fieldwork", title: "Fieldwork", description: "
|
|
293
|
+
{ id: "engagement", title: "Engagement", description: "Record the actual CPA engagement, formal scope and dates, requests, and management responses.", steps: ["After engaging the CPA firm, create the Audit with its agreed type, scope, and date or period.", "Track firm requests with owners, due dates, and approved responses."], types: ["audit", "audit-request"], defaultOpen: true },
|
|
294
|
+
{ id: "fieldwork", title: "Fieldwork", description: "Complete engagement documents, populations, requests, and the evidence packet.", steps: ["Complete engagement Documents and, for Type 2, reconcile each population to its fixed export.", "Review dated operating records and verified Evidence Artifacts for the agreed period; link samples and answer requests.", "Build and review the indexed packet from a clean Git revision."], types: ["audit-population", "control-test"], relatedLinks: [{ type: "document", label: "Audit Documents", href: "#/resources/document?stage=audit&documentScope=audit", summary: "Complete and approve engagement Documents, then link the required ones from each Audit.", instructions: "Complete each required engagement Document, link it from its Audit, obtain separate approval of the exact Markdown revision, then activate that approved revision for that Audit.", output: "Active Audit Documents linked to their Audits and bound to their approved revisions." }], utility: "audit-packet", defaultOpen: true }
|
|
245
295
|
],
|
|
246
296
|
resourceTypes: ["audit", "audit-request", "audit-population", "control-test"],
|
|
247
297
|
utilities: [
|
|
@@ -257,9 +307,12 @@ export const PROGRAM_PATH = [
|
|
|
257
307
|
],
|
|
258
308
|
commands: [
|
|
259
309
|
"filegrc guide audit --json",
|
|
310
|
+
"filegrc guide document --json",
|
|
311
|
+
"filegrc list document --json",
|
|
260
312
|
"filegrc scaffold audit --title \"YEAR SOC 2 TYPE\"",
|
|
261
313
|
"filegrc create AUDIT-MUTATION.json --json",
|
|
262
314
|
"filegrc prepare-audit AUDIT_ID --json",
|
|
315
|
+
"filegrc activate-documents --scaffold --audit AUDIT_ID",
|
|
263
316
|
"filegrc audit-readiness AUDIT_ID --json",
|
|
264
317
|
"filegrc evidence-packet --audit AUDIT_ID --preview --json"
|
|
265
318
|
]
|
|
@@ -321,14 +374,27 @@ export function buildAgentProgramPath(model) {
|
|
|
321
374
|
title: definition.pluralTitle,
|
|
322
375
|
summary: RESOURCE_PAGE_SUMMARIES[type] || definition.description,
|
|
323
376
|
instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
|
|
377
|
+
output: RESOURCE_OUTPUTS[type] || `A validated ${definition.title} record.`,
|
|
324
378
|
use: definition.description,
|
|
325
379
|
policyBasis: definition.guidance.policyBasis,
|
|
326
380
|
guide: `npx filegrc guide ${type} --json`,
|
|
327
381
|
list: `npx filegrc list ${type} --json`
|
|
328
382
|
};
|
|
329
383
|
});
|
|
384
|
+
const relatedPages = stage.sections.flatMap((section) => section.relatedLinks || [])
|
|
385
|
+
.filter((link) => link.instructions)
|
|
386
|
+
.map((link, index) => ({
|
|
387
|
+
order: `${stage.number}.${String.fromCharCode(97 + stage.resourceTypes.length + index)}`,
|
|
388
|
+
type: link.type,
|
|
389
|
+
title: link.label,
|
|
390
|
+
summary: link.summary,
|
|
391
|
+
instructions: link.instructions,
|
|
392
|
+
output: link.output,
|
|
393
|
+
guide: `npx filegrc program-path --json`,
|
|
394
|
+
list: `npx filegrc list ${link.type} --json`
|
|
395
|
+
}));
|
|
330
396
|
const utilityPages = (stage.utilities || []).map((utility, index) => ({
|
|
331
|
-
order: stage.id === "run" ? null : `${stage.number}.${String.fromCharCode(97 + stage.resourceTypes.length + index)}`,
|
|
397
|
+
order: stage.id === "run" ? null : `${stage.number}.${String.fromCharCode(97 + stage.resourceTypes.length + relatedPages.length + index)}`,
|
|
332
398
|
utility: utility.id,
|
|
333
399
|
title: utility.title,
|
|
334
400
|
summary: utility.summary,
|
|
@@ -340,7 +406,7 @@ export function buildAgentProgramPath(model) {
|
|
|
340
406
|
return {
|
|
341
407
|
...stage,
|
|
342
408
|
commands: stage.commands.map(agentCommand),
|
|
343
|
-
pages: stage.id === "run" ? utilityPages : [...resourcePages, ...utilityPages],
|
|
409
|
+
pages: stage.id === "run" ? utilityPages : [...resourcePages, ...relatedPages, ...utilityPages],
|
|
344
410
|
...(stage.id === "run" ? { operatingRecords: resourcePages } : {})
|
|
345
411
|
};
|
|
346
412
|
});
|
package/src/program-readiness.js
CHANGED
|
@@ -1508,10 +1508,12 @@ function legacyPolicyLibraryProposals(records) {
|
|
|
1508
1508
|
|
|
1509
1509
|
async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
1510
1510
|
const items = [];
|
|
1511
|
+
const families = selectedControlFamilies(scope.controls, model);
|
|
1511
1512
|
if (!scope.controls.length) {
|
|
1512
1513
|
items.push(item("control-scope", "action", "Select the program controls", "No controls are selected for the management program.", { type: "control" }));
|
|
1513
1514
|
}
|
|
1514
1515
|
for (const control of scope.controls) {
|
|
1516
|
+
const controlFamilies = families.filter((family) => family.controls.some(({ id }) => id === control.id));
|
|
1515
1517
|
const source = await readMarkdown(control);
|
|
1516
1518
|
const sourceSystems = (
|
|
1517
1519
|
modelSupports(model, "component-sources")
|
|
@@ -1523,6 +1525,18 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1523
1525
|
&& record.status !== "retired"
|
|
1524
1526
|
&& (record.controlIds || []).includes(control.id)
|
|
1525
1527
|
));
|
|
1528
|
+
const sourceDetails = await Promise.all(sourceSystems.map(async (sourceRecord) => ({
|
|
1529
|
+
record: sourceRecord,
|
|
1530
|
+
instructions: await readMarkdown(sourceRecord)
|
|
1531
|
+
})));
|
|
1532
|
+
const missingSourceFamilies = controlFamilies.filter((family) => !sourceDetails.some(({ record, instructions }) => (
|
|
1533
|
+
record.status === "active"
|
|
1534
|
+
&& (record.evidenceSourceKinds || []).length > 0
|
|
1535
|
+
&& (!family.sourceKinds.length || family.sourceKinds.some((kind) => (record.evidenceSourceKinds || []).includes(kind)))
|
|
1536
|
+
&& (record.evidenceOwnerIds || []).length > 0
|
|
1537
|
+
&& substantiveMarkdown(instructions)
|
|
1538
|
+
&& openPlaceholderCount(instructions) === 0
|
|
1539
|
+
)));
|
|
1526
1540
|
const checks = {
|
|
1527
1541
|
...(model.resources.control?.fields?.applicabilityReview ? {
|
|
1528
1542
|
applicability: control.applicabilityReview?.decision === "applicable"
|
|
@@ -1534,6 +1548,9 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1534
1548
|
scope: (control.systemIds || []).some((id) => scope.systems.some((system) => system.id === id)),
|
|
1535
1549
|
operationPattern: Boolean(control.operationPattern),
|
|
1536
1550
|
evidenceSource: sourceSystems.length > 0,
|
|
1551
|
+
...(modelSupports(model, "component-sources") ? {
|
|
1552
|
+
evidenceSourceReady: missingSourceFamilies.length === 0
|
|
1553
|
+
} : {}),
|
|
1537
1554
|
implementationDate: Boolean(control.effectiveOn && control.effectiveOn <= asOf),
|
|
1538
1555
|
...(model.resources.control?.fields?.procedureRevision ? {
|
|
1539
1556
|
procedureRevision: Boolean(control.procedureRevision),
|
|
@@ -1547,25 +1564,21 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1547
1564
|
} : {})
|
|
1548
1565
|
};
|
|
1549
1566
|
const missing = Object.entries(checks).filter(([, value]) => !value).map(([name]) => controlCheckLabel(name));
|
|
1567
|
+
const nextSteps = controlImplementationSteps(control, checks, {
|
|
1568
|
+
missingSourceFamilies
|
|
1569
|
+
});
|
|
1550
1570
|
items.push(item(
|
|
1551
1571
|
`control-${control.id}`,
|
|
1552
1572
|
missing.length ? "action" : "complete",
|
|
1553
1573
|
`${control.code ? `${control.code}: ` : ""}${control.title}`,
|
|
1554
1574
|
missing.length
|
|
1555
|
-
? `Complete
|
|
1575
|
+
? nextSteps[0] || `Complete the remaining Control checks: ${missing.join(", ")}.`
|
|
1556
1576
|
: `Implemented ${control.effectiveOn}; owned, scoped, scheduled, documented, mapped, and tied to ${sourceSystems.length} authoritative ${sourceSystems.length === 1 ? "source" : "sources"}.`,
|
|
1557
1577
|
control,
|
|
1558
1578
|
{
|
|
1559
1579
|
checks,
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
"npx filegrc review-applicability --scaffold --type control > control-decisions.json",
|
|
1563
|
-
"npx filegrc review-applicability control-decisions.json --preview --json",
|
|
1564
|
-
"npx filegrc review-applicability control-decisions.json --yes --json"
|
|
1565
|
-
] : []),
|
|
1566
|
-
"npx filegrc evidence-map --json",
|
|
1567
|
-
`npx filegrc get ${shellArgument(control.id)} --mutation`
|
|
1568
|
-
],
|
|
1580
|
+
nextSteps,
|
|
1581
|
+
commands: [`npx filegrc get ${shellArgument(control.id)} --mutation`],
|
|
1569
1582
|
workQueue: queueSchedules.length ? {
|
|
1570
1583
|
enabled: queueSchedules.filter(obligationIsEnabled).length,
|
|
1571
1584
|
running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf, model)).length,
|
|
@@ -1574,7 +1587,7 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1574
1587
|
}
|
|
1575
1588
|
));
|
|
1576
1589
|
}
|
|
1577
|
-
return stage("controls", "Implement Controls", "
|
|
1590
|
+
return stage("controls", "Implement Controls", "Put each selected Control in place, record how it works, and mark it Implemented when it is working.", items);
|
|
1578
1591
|
}
|
|
1579
1592
|
|
|
1580
1593
|
async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
@@ -1930,6 +1943,7 @@ function controlCheckLabel(name) {
|
|
|
1930
1943
|
scope: "in-scope systems",
|
|
1931
1944
|
operationPattern: "operation pattern",
|
|
1932
1945
|
evidenceSource: "authoritative evidence source",
|
|
1946
|
+
evidenceSourceReady: "usable evidence source",
|
|
1933
1947
|
implementationDate: "implementation date",
|
|
1934
1948
|
procedureRevision: "effective procedure revision",
|
|
1935
1949
|
procedureEffective: "procedure effective date",
|
|
@@ -1939,6 +1953,76 @@ function controlCheckLabel(name) {
|
|
|
1939
1953
|
})[name] || name;
|
|
1940
1954
|
}
|
|
1941
1955
|
|
|
1956
|
+
const starterControlSetup = {
|
|
1957
|
+
"control-security-governance": "Name a reviewer outside day-to-day security ownership and set up a quarterly security review.",
|
|
1958
|
+
"control-policy-management": "Assign someone to maintain the Control list and a separate approver for policies and plans. Set up their review path.",
|
|
1959
|
+
"control-security-communication": "Set up how the approved reporting routes, policy changes, and security notices reach staff and outside parties.",
|
|
1960
|
+
"control-workforce-expectations": "Set up the checks, agreements, and policy acknowledgements required before someone receives sensitive access.",
|
|
1961
|
+
"control-security-training": "Choose how new and existing workers receive security training, acknowledge the current material, and get reminders.",
|
|
1962
|
+
"control-risk-assessment": "Use the Program's risk method to assess the scoped service and record the results in FileGRC Risk Assessments and Risks.",
|
|
1963
|
+
"control-monitoring-remediation": "Define how Control problems are reported, reviewed, and verified closed. Use FileGRC Findings for gaps that need separate follow-up.",
|
|
1964
|
+
"control-access-authorization": "Configure unique, least-privilege accounts in the identity system. Restrict account changes to authorized admins.",
|
|
1965
|
+
"control-strong-authentication": "Turn on required MFA and secure sign-in settings for the in-scope Systems. Identify any gap that needs an approved Exception.",
|
|
1966
|
+
"control-access-review-offboarding": "Confirm the identity system can produce a complete access list and admins can promptly remove or change permissions.",
|
|
1967
|
+
"control-physical-workspace-security": "Decide how visitors, work areas, devices, and paper records are protected in the places your team actually works.",
|
|
1968
|
+
"control-data-classification-inventory": "Apply the approved Classifications to important data in the in-scope Systems and record its location in the inventory.",
|
|
1969
|
+
"control-encryption-transmission": "Configure encryption for in-scope data and devices, and assign who manages keys and approved transfer methods.",
|
|
1970
|
+
"control-data-retention-disposal": "Apply the approved retention schedule to live, backup, and vendor-held data. Define how each class is deleted or destroyed.",
|
|
1971
|
+
"control-inventory-configuration": "List important assets and owners, choose their secure settings, and decide how unsupported assets are handled.",
|
|
1972
|
+
"control-endpoint-protection": "Apply the required device settings, updates, encryption, screen lock, and malware protection to devices that access company Systems.",
|
|
1973
|
+
"control-network-security": "Restrict production network paths and remote access, and document the rules that allow traffic between environments.",
|
|
1974
|
+
"control-change-management": "Set up a change path with risk review, testing, approval, deployment, and rollback for software and infrastructure changes.",
|
|
1975
|
+
"control-vulnerability-management": "Choose what will be scanned, how often, who handles findings, and the target times for fixing them.",
|
|
1976
|
+
"control-penetration-testing": "Decide whether independent testing is needed for this service. If it is, set its scope, cadence, and owner.",
|
|
1977
|
+
"control-logging-monitoring": "Enable useful security logs and alerts for important Systems, protect them, and choose who responds to alerts.",
|
|
1978
|
+
"control-incident-response": "Set up a way to report, assign, escalate, contain, and close security incidents using the approved response plan.",
|
|
1979
|
+
"control-incident-exercise": "Choose a realistic incident scenario and an alert path to test, and assign the people who will run the exercise.",
|
|
1980
|
+
"control-backup-restoration": "Choose what needs backup or another recovery path, configure it, and decide how restores will be tested.",
|
|
1981
|
+
"control-continuity-exercise": "Set recovery priorities, contacts, and responsibilities, and choose how the continuity plan will be exercised.",
|
|
1982
|
+
"control-vendor-due-diligence": "Set a security review and contract check before a new Vendor gets sensitive data or becomes a material dependency.",
|
|
1983
|
+
"control-vendor-monitoring": "Choose which Vendors need ongoing review, who reviews them, and what information the review uses.",
|
|
1984
|
+
"control-security-exceptions": "Document how an Exception is reviewed and approved before a departure from policy begins. Record actual decisions in FileGRC Exceptions."
|
|
1985
|
+
};
|
|
1986
|
+
|
|
1987
|
+
export function controlImplementationSteps(control, checks, options = {}) {
|
|
1988
|
+
const steps = [];
|
|
1989
|
+
if (!checks.implemented) steps.push(starterControlSetup[control.id]
|
|
1990
|
+
|| `Set up this Control: ${control.activity || control.statement}`);
|
|
1991
|
+
const record = [];
|
|
1992
|
+
if (!checks.owner) record.push("choose an owner");
|
|
1993
|
+
if (!checks.scope) record.push("select the Systems this covers");
|
|
1994
|
+
if (!checks.operationPattern) record.push("set when it runs");
|
|
1995
|
+
if (!checks.procedure) record.push("write who does what and when in Procedure");
|
|
1996
|
+
if (!checks.evidenceSource) record.push("link the tool or system that can show it happened");
|
|
1997
|
+
if (!checks.policyMapping && checks.policyMapping !== undefined) record.push("link the Policy it implements");
|
|
1998
|
+
if (!checks.criteriaMapping && checks.criteriaMapping !== undefined) record.push("link the criteria it covers");
|
|
1999
|
+
if (checks.implemented && !checks.implementationDate) record.push("enter the date it started working");
|
|
2000
|
+
if (checks.implemented && (checks.procedureRevision === false || checks.procedureEffective === false)) {
|
|
2001
|
+
record.push("record the Procedure revision and effective date");
|
|
2002
|
+
}
|
|
2003
|
+
if (record.length) steps.push(`In this Control, ${record.join("; ")}.`);
|
|
2004
|
+
const sourceFamilies = options.missingSourceFamilies || [];
|
|
2005
|
+
const sourceTargets = sourceFamilies.map((family) => `${family.title}${family.sourceKinds?.length ? ` (${family.sourceKinds.join(" or ")})` : ""}`);
|
|
2006
|
+
const sourceStep = checks.evidenceSourceReady === false
|
|
2007
|
+
? `Cover the missing evidence families: ${sourceTargets.length ? sourceTargets.join(" and ") : "this Control"}. For each, link an active Component with the matching kind, evidence owners, and report retrieval instructions.`
|
|
2008
|
+
: null;
|
|
2009
|
+
const queueStep = checks.workQueue === false
|
|
2010
|
+
? "Enable a linked Obligation with the owner and schedule for this Control's recurring or event work."
|
|
2011
|
+
: null;
|
|
2012
|
+
if (!checks.implemented) {
|
|
2013
|
+
if (sourceStep || queueStep) steps.push([sourceStep, queueStep].filter(Boolean).join(" "));
|
|
2014
|
+
else if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
|
|
2015
|
+
else steps.push(checks.procedureRevision === undefined
|
|
2016
|
+
? "When it works, mark this Control Implemented and enter its start date."
|
|
2017
|
+
: "When it works, mark this Control Implemented. Enter its start date and the Procedure revision and effective date.");
|
|
2018
|
+
} else {
|
|
2019
|
+
if (sourceStep) steps.push(sourceStep);
|
|
2020
|
+
if (queueStep) steps.push(queueStep);
|
|
2021
|
+
if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
|
|
2022
|
+
}
|
|
2023
|
+
return steps;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1942
2026
|
function assuranceGoalLabel(goal) {
|
|
1943
2027
|
if (goal === "soc-2-type-1") return "SOC 2 Type 1";
|
|
1944
2028
|
if (goal === "soc-2-type-2") return "SOC 2 Type 2";
|
package/src/state.js
CHANGED
|
@@ -277,6 +277,7 @@ async function createAppStateUnlocked(input, options) {
|
|
|
277
277
|
(audits.length ? audits : [null]).map(async (audit) => {
|
|
278
278
|
const preparation = await assessAuditPreparation(loaded, {
|
|
279
279
|
auditId: audit?.id,
|
|
280
|
+
programId: activeProgram.id,
|
|
280
281
|
asOf,
|
|
281
282
|
generatedAt,
|
|
282
283
|
...(audit ? {} : { programReadiness })
|
package/src/web.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
utcCalendarDate,
|
|
11
11
|
validCalendarRecurrence
|
|
12
12
|
} from "./recurrence.js";
|
|
13
|
-
import { PROGRAM_PATH, programPathForModel, RESOURCE_INSTRUCTIONS, RESOURCE_PAGE_SUMMARIES } from "./program-path.js";
|
|
13
|
+
import { PROGRAM_PATH, programPathForModel, RESOURCE_INSTRUCTIONS, RESOURCE_OUTPUTS, RESOURCE_PAGE_SUMMARIES } from "./program-path.js";
|
|
14
14
|
import { formatCalendarDate, formatLocalDateTime } from "./time.js";
|
|
15
15
|
|
|
16
16
|
export function dashboardProgramReadiness(programReadiness = {}) {
|
|
@@ -96,6 +96,7 @@ let READINESS_STAGES = SHARED_PROGRAM_STAGES.map((stage) => ({
|
|
|
96
96
|
number: String(stage.number)
|
|
97
97
|
}));
|
|
98
98
|
const RESOURCE_GUIDE_INSTRUCTIONS = ${JSON.stringify(RESOURCE_INSTRUCTIONS)};
|
|
99
|
+
const RESOURCE_GUIDE_OUTPUTS = ${JSON.stringify(RESOURCE_OUTPUTS)};
|
|
99
100
|
const dashboardProgramReadiness = ${dashboardProgramReadiness.toString()};
|
|
100
101
|
const workflowHrefBelongsToPage = ${workflowHrefBelongsToPage.toString()};
|
|
101
102
|
const pageActionIdentity = ${pageActionIdentity.toString()};
|
|
@@ -246,6 +247,7 @@ function normalizeAppState(next) {
|
|
|
246
247
|
function blockingStateSections(route) {
|
|
247
248
|
if (route.name === "home") return ["program", "obligations", "workflow"];
|
|
248
249
|
if (route.name === "repository") return ["repository"];
|
|
250
|
+
if (route.name === "list" && route.type === "control") return ["program"];
|
|
249
251
|
if (route.name === "list" && route.type === "obligation") return ["obligations"];
|
|
250
252
|
if (route.name === "obligations" || route.name === "stage" && route.stageId === "run") return ["program", "obligations"];
|
|
251
253
|
if (route.name === "audit-packet") return ["repository", "program", "obligations", "audits"];
|
|
@@ -613,7 +615,7 @@ function renderStageOverview(main, stageId, params = new URLSearchParams()) {
|
|
|
613
615
|
const progress = stageProgress(stage);
|
|
614
616
|
main.innerHTML = '<div class="page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
|
|
615
617
|
'<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(progress) + '</section>' +
|
|
616
|
-
renderStagePageIndex(stage) + (stage.id === "controls" ? renderFinishStepThree() : "") + (stage.id === "audit" ? renderAuditDocumentActivationAssessments() : "") + '</div>';
|
|
618
|
+
renderStageInstructions(stage) + renderStagePageIndex(stage) + (stage.id === "controls" ? renderFinishStepThree() : "") + (stage.id === "audit" ? renderAuditDocumentActivationAssessments() : "") + '</div>';
|
|
617
619
|
main.querySelector("[data-show-evidence-families]")?.addEventListener("click", (event) => {
|
|
618
620
|
main.querySelectorAll("[data-evidence-family-extra]").forEach((card) => { card.hidden = false; });
|
|
619
621
|
event.currentTarget.remove();
|
|
@@ -634,7 +636,8 @@ function renderPoliciesPage(main, params = new URLSearchParams()) {
|
|
|
634
636
|
}
|
|
635
637
|
|
|
636
638
|
function policyContentGuide() {
|
|
637
|
-
|
|
639
|
+
const section = READINESS_STAGES.find(({ id }) => id === "policies")?.sections.find(({ id }) => id === "policy-content");
|
|
640
|
+
return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use Policies" hidden><div><span>Do now</span><p>' + esc(section?.instructions || "Review and approve the program's governing content.") + '</p></div><div><span>Output</span><p>' + esc(section?.output || "Approved content bound to the reviewed revisions.") + '</p></div></section>';
|
|
638
641
|
}
|
|
639
642
|
|
|
640
643
|
function renderRetentionSchedulePage(main, params = new URLSearchParams()) {
|
|
@@ -846,7 +849,7 @@ function renderPolicyApprovalGuidance() {
|
|
|
846
849
|
: "";
|
|
847
850
|
return '<article><strong>' + esc(proposal.title) + '</strong><p>' + esc(proposal.message) + '</p>' + review + '<div class="evidence-map-references">' + proposal.policyIds.map((id) => formatReference(id)).join("") + '</div></article>';
|
|
848
851
|
}).join("");
|
|
849
|
-
return '<section class="policy-lifecycle-note panel ' + (proposalRows ? "" : "single") + '"><div><p class="kicker">Step 2 approval</p><h3>Approve the governed content</h3><p>
|
|
852
|
+
return '<section class="policy-lifecycle-note panel ' + (proposalRows ? "" : "single") + '"><div><p class="kicker">Step 2 approval</p><h3>Approve the governed content</h3><p>Replace placeholders and confirm the intended values. Have a separate approver review the exact Markdown revision and record the approval date.</p></div>' + (proposalRows ? '<div class="policy-library-proposals">' + proposalRows + '</div>' : "") + '</section>';
|
|
850
853
|
}
|
|
851
854
|
|
|
852
855
|
function renderPoliciesTable(params = new URLSearchParams()) {
|
|
@@ -963,7 +966,8 @@ function renderDocumentActivationAssessments() {
|
|
|
963
966
|
}
|
|
964
967
|
|
|
965
968
|
function renderAuditDocumentActivationAssessments() {
|
|
966
|
-
const audits = resourcesOfType("audit").map(({ record }) => record)
|
|
969
|
+
const audits = resourcesOfType("audit").map(({ record }) => record)
|
|
970
|
+
.filter((record) => !state.selectedProgramId || record.programId === state.selectedProgramId);
|
|
967
971
|
const audit = selectDefaultAudit(audits, state.programReadiness?.asOf || currentDate());
|
|
968
972
|
const preparation = audit ? state.auditPreparations?.[audit.id] : null;
|
|
969
973
|
const assessments = preparation?.documentActivations || [];
|
|
@@ -1182,7 +1186,10 @@ function workflowGuidance(options = {}) {
|
|
|
1182
1186
|
const visible = items.slice(0, 6);
|
|
1183
1187
|
const renderRow = (item) => {
|
|
1184
1188
|
const href = workflowItemHref(item);
|
|
1185
|
-
const
|
|
1189
|
+
const guidance = options.type === "control" && item.nextSteps?.length
|
|
1190
|
+
? '<ol class="control-next-steps">' + item.nextSteps.map((step) => '<li>' + esc(step) + '</li>').join("") + '</ol>'
|
|
1191
|
+
: '<small>' + esc(item.message || workflowItemDetail(item)) + '</small>';
|
|
1192
|
+
const body = '<span class="workflow-finding-status ' + esc(item.state) + '">' + esc(properCase(item.state)) + '</span><span><strong>' + esc(item.title) + '</strong>' + guidance + '</span>';
|
|
1186
1193
|
return href ? '<a href="' + href + '">' + body + '</a>' : '<div>' + body + '</div>';
|
|
1187
1194
|
};
|
|
1188
1195
|
const rows = visible.map(renderRow).join("");
|
|
@@ -1750,6 +1757,12 @@ function renderStagePageIndex(stage) {
|
|
|
1750
1757
|
return '<section class="stage-pages" aria-label="Step ' + esc(stage.number) + ' pages"><div class="stage-page-grid">' + cards + '</div></section>';
|
|
1751
1758
|
}
|
|
1752
1759
|
|
|
1760
|
+
function renderStageInstructions(stage) {
|
|
1761
|
+
return '<details class="stage-instructions"><summary>How to complete Step ' + esc(stage.number) + '</summary><div class="stage-instructions-body">' + stage.sections.map((section) => (
|
|
1762
|
+
'<section><h3>' + esc(section.title) + '</h3><ol>' + section.steps.map((step) => '<li>' + esc(step) + '</li>').join("") + '</ol></section>'
|
|
1763
|
+
)).join("") + '</div></details>';
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1753
1766
|
function stagePageDestinations(stage) {
|
|
1754
1767
|
return stage.sections.flatMap((section) => sectionDestinations(section)
|
|
1755
1768
|
.filter((destination) => destination.utility !== "obligation-board")
|
|
@@ -1820,7 +1833,7 @@ function derivedStagePageState(stage, destination) {
|
|
|
1820
1833
|
if (
|
|
1821
1834
|
stage.id === "audit"
|
|
1822
1835
|
&& state.workflow?.assessments?.auditReadiness?.status === "not-started"
|
|
1823
|
-
&& (destination.type === "audit" || destination.utility === "audit-packet")
|
|
1836
|
+
&& (destination.type === "audit" || destination.type === "document" && destination.href.includes("documentScope=audit") || destination.utility === "audit-packet")
|
|
1824
1837
|
) {
|
|
1825
1838
|
return { complete: false, label: "No engagement" };
|
|
1826
1839
|
}
|
|
@@ -1855,6 +1868,11 @@ function derivedStagePageState(stage, destination) {
|
|
|
1855
1868
|
? { complete: true, label: "Reviewed" }
|
|
1856
1869
|
: { complete: false, label: "Review scope" };
|
|
1857
1870
|
}
|
|
1871
|
+
if (stage.id === "audit" && destination.type === "document" && destination.href.includes("documentScope=audit")) {
|
|
1872
|
+
if (!resourcesOfType("document").some(({ record }) => auditSpecificDocument(record))) {
|
|
1873
|
+
return { complete: false, countsTowardProgress: false, label: "Only if needed" };
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1858
1876
|
if (destination.type && resourcesOfType(destination.type).length === 0) {
|
|
1859
1877
|
return { complete: false, countsTowardProgress: false, label: "Only if needed" };
|
|
1860
1878
|
}
|
|
@@ -2005,7 +2023,7 @@ function sectionDestinations(section) {
|
|
|
2005
2023
|
destinations.push({ type, kind: "Record page", label: titleCase(definition.pluralTitle), href: "#/resources/" + encodeURIComponent(type), description: definition.description });
|
|
2006
2024
|
}
|
|
2007
2025
|
for (const link of section.relatedLinks || []) {
|
|
2008
|
-
destinations.push({ type: link.type, kind: "Record page", label: link.label, href: link.href, description: section.description });
|
|
2026
|
+
destinations.push({ type: link.type, kind: "Record page", label: link.label, href: link.href, description: link.summary || section.description });
|
|
2009
2027
|
}
|
|
2010
2028
|
if (section.utility === "obligation-board") destinations.push({ utility: section.utility, kind: "Working page", label: "Work Queue", href: "#/stage/run", description: "Complete recurring work, Policy Event tasks, and assigned follow-up with its due windows and linked proof." });
|
|
2011
2029
|
if (section.utility === "evidence-sources") destinations.push({ utility: section.utility, kind: "Coverage page", label: "Evidence Sources", href: "#/evidence-sources", description: section.description });
|
|
@@ -2083,6 +2101,7 @@ function renderObligations(main, params = new URLSearchParams()) {
|
|
|
2083
2101
|
: "";
|
|
2084
2102
|
main.innerHTML = '<div class="page obligation-board-page stage-overview-page"><nav class="breadcrumbs"><a href="#/">Overview</a><span>/</span><span>' + esc(stage.title) + '</span></nav>' +
|
|
2085
2103
|
'<section class="stage-overview-hero"><div><p class="kicker">Step ' + esc(stage.number) + ' of 5</p><h2>' + esc(stage.title) + '</h2><p>' + esc(stage.summary) + '</p></div>' + stageProgressCard(stageProgress(stage)) + '</section>' +
|
|
2104
|
+
renderStageInstructions(stage) +
|
|
2086
2105
|
feedback +
|
|
2087
2106
|
operationGate +
|
|
2088
2107
|
activeOperation +
|
|
@@ -2744,6 +2763,13 @@ function openNextAuditCycleDialog(prior) {
|
|
|
2744
2763
|
});
|
|
2745
2764
|
}
|
|
2746
2765
|
|
|
2766
|
+
function controlApplicabilityPending(record) {
|
|
2767
|
+
if (!record.applicabilityReview) return true;
|
|
2768
|
+
const item = state.programReadiness?.stages?.find(({ id }) => id === "controls")?.items
|
|
2769
|
+
.find(({ resourceType, resourceId }) => resourceType === "control" && resourceId === record.id);
|
|
2770
|
+
return item?.checks?.applicability === false;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2747
2773
|
function openApplicabilityReviewDialog(type, entries) {
|
|
2748
2774
|
const definition = state.model.resources[type];
|
|
2749
2775
|
const reviewPoints = definition?.guidance?.reviewPoints || [];
|
|
@@ -2752,8 +2778,9 @@ function openApplicabilityReviewDialog(type, entries) {
|
|
|
2752
2778
|
const pending = entries.filter(({ record }) => (
|
|
2753
2779
|
type === "requirement" && modelSupports("program-scope")
|
|
2754
2780
|
? !reviewedRequirements.has(record.id)
|
|
2755
|
-
:
|
|
2756
|
-
|
|
2781
|
+
: type === "control"
|
|
2782
|
+
? controlApplicabilityPending(record)
|
|
2783
|
+
: !record.applicabilityReview || type === "requirement" && record.applicability === "undetermined"
|
|
2757
2784
|
));
|
|
2758
2785
|
const dialog = document.createElement("dialog");
|
|
2759
2786
|
dialog.className = "commit-dialog applicability-dialog";
|
|
@@ -3102,7 +3129,9 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
3102
3129
|
const hasPendingApplicability = entries.some(({ record }) => (
|
|
3103
3130
|
type === "requirement" && modelSupports("program-scope")
|
|
3104
3131
|
? !reviewedRequirements.has(record.id)
|
|
3105
|
-
:
|
|
3132
|
+
: type === "control"
|
|
3133
|
+
? controlApplicabilityPending(record)
|
|
3134
|
+
: !record.applicabilityReview || type === "requirement" && record.applicability === "undetermined"
|
|
3106
3135
|
));
|
|
3107
3136
|
const applicabilityButton = !state.readOnly
|
|
3108
3137
|
&& ["requirement", "control", "commitment", "complementary-control"].includes(type)
|
|
@@ -3112,8 +3141,11 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
3112
3141
|
const guideTrigger = '<button class="guide-trigger" id="resource-guide-trigger" type="button" aria-label="About ' + esc(definition.pluralTitle) + '" aria-haspopup="dialog" aria-controls="resource-guide" aria-expanded="false"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8"></circle><path d="M7.8 7.5a2.4 2.4 0 1 1 3.25 2.25c-.7.31-1.05.72-1.05 1.5v.25M10 14.5v.1"></path></svg></button>';
|
|
3113
3142
|
const listTools = '<div class="list-tools list-header-tools"><label><span class="sr-only">Filter list</span><input id="list-search" type="search" placeholder="Filter ' + esc(definition.pluralTitle.toLowerCase()) + '"></label>' +
|
|
3114
3143
|
filters.map(({ name, label, values }) => '<select class="field-filter" data-field="' + esc(name) + '" aria-label="Filter by ' + esc(label.toLowerCase()) + '"><option value="">Any ' + esc(properCase(label)) + '</option>' + values.map((value) => '<option value="' + esc(value) + '">' + esc(filterOptionLabel(value)) + '</option>').join("") + '</select>').join("") + '<span id="result-count" aria-live="polite">' + entries.length + ' records</span>' + applicabilityButton + createButton + '</div>';
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3144
|
+
const relatedGuidance = type === "document" && documentScope === "audit"
|
|
3145
|
+
? listStage?.sections.flatMap((section) => section.relatedLinks || []).find((link) => link.type === "document" && link.instructions)
|
|
3146
|
+
: null;
|
|
3147
|
+
const pageSummary = relatedGuidance?.summary || STAGE_PAGE_SUMMARIES[type] || definition.description;
|
|
3148
|
+
main.innerHTML = '<div class="page"><div class="page-intro"><div><p class="kicker">' + esc(listStage?.title || groupTitle(definition.group)) + '</p><div class="page-title-line"><h2>' + esc(titleCase(listTitle)) + '</h2>' + guideTrigger + '</div><p>' + esc(pageSummary) + '</p></div>' + listTools + '</div>' + resourceGuide(type, relatedGuidance) +
|
|
3117
3149
|
collectionReviewPanel(type) +
|
|
3118
3150
|
'<section class="record-table-wrap"><table class="record-table"><thead><tr><th>' + esc(fieldLabel(type, "title")) + '</th>' + fields.map((name) => '<th>' + esc(fieldLabel(type, name)) + '</th>').join("") + '<th>Next action</th><th>Git file</th></tr></thead><tbody id="record-rows"></tbody></table></section>' +
|
|
3119
3151
|
'<nav class="pagination list-pagination" aria-label="' + esc(definition.pluralTitle) + ' pages" hidden><button class="button" type="button" data-page="previous">Previous</button><span class="page-status" aria-live="polite"></span><button class="button" type="button" data-page="next">Next</button></nav></div>';
|
|
@@ -3316,7 +3348,7 @@ function renderDetail(main, type, id, params = new URLSearchParams()) {
|
|
|
3316
3348
|
});
|
|
3317
3349
|
const mayDelete = !state.readOnly && !routeSetLocked && !definition.singleton;
|
|
3318
3350
|
const recordMenu = '<details class="record-overflow"><summary aria-label="More record actions" title="More record actions"><svg viewBox="0 0 18 4" aria-hidden="true"><circle cx="2" cy="2" r="2"></circle><circle cx="9" cy="2" r="2"></circle><circle cx="16" cy="2" r="2"></circle></svg></summary><div class="record-overflow-menu" aria-label="Record actions"><button type="button" data-record-modal="info">Record info</button><button type="button" data-record-modal="connections">Connections</button><button type="button" data-record-modal="history"' + (entry.historyLoaded === false ? ' disabled' : '') + '>File history' + (entry.historyLoaded === false ? ' (loading)' : '') + '</button>' + (mayDelete ? '<div class="record-overflow-separator"></div><button class="danger-text" type="button" data-delete-resource>Delete record</button>' : '') + '</div></details>';
|
|
3319
|
-
main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="' + collectionHref + '">' + esc(collectionLabel) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + reportingRouteSetActions + auditCycleAction + auditPopulationCorrectionAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence & Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly && !routeSetLocked ? '<button class="button" id="edit-resource">Edit</button>' : "") + recordMenu + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + '">' + detailMain + supportPanels + '</div></div>';
|
|
3351
|
+
main.innerHTML = '<div class="page"><div class="detail-head"><div><div class="breadcrumbs header-breadcrumbs"><a href="' + collectionHref + '">' + esc(collectionLabel) + '</a><span>/</span><span>' + esc(entry.record.title) + '</span></div><h2>' + esc(titleCase(entry.record.title)) + '</h2></div><div class="actions">' + reportingRouteSetActions + auditCycleAction + auditPopulationCorrectionAction + (type === "audit" ? '<a class="button primary" href="#/audit-packet?auditId=' + encodeURIComponent(entry.record.id) + '">Audit Evidence & Packet</a>' : "") + governanceActions + lifecycleActions + issueActions + addRecordContentAction + (!state.readOnly && !routeSetLocked ? '<button class="button" id="edit-resource">Edit</button>' : "") + recordMenu + '</div></div><div class="detail-grid ' + (hasRecordBody ? "" : "detail-grid-structured") + (type === "control" ? " detail-control" : "") + '">' + detailMain + supportPanels + '</div></div>';
|
|
3320
3352
|
main.querySelectorAll("[data-completion-requirements]").forEach((button) => button.addEventListener("click", () => openCompletionRequirementsDialog(button.dataset.completionRequirements)));
|
|
3321
3353
|
main.querySelector("#edit-resource")?.addEventListener("click", () => openEditor(type, entry));
|
|
3322
3354
|
main.querySelectorAll("[data-record-modal]").forEach((button) => button.addEventListener("click", () => {
|
|
@@ -4251,11 +4283,11 @@ function zonedTimestampFromLocal(value, timezone) {
|
|
|
4251
4283
|
return value + offset;
|
|
4252
4284
|
}
|
|
4253
4285
|
|
|
4254
|
-
function resourceGuide(type) {
|
|
4286
|
+
function resourceGuide(type, pageGuidance = null) {
|
|
4255
4287
|
const definition = state.model.resources[type];
|
|
4256
4288
|
const guidance = definition?.guidance;
|
|
4257
4289
|
if (!definition || !guidance) return "";
|
|
4258
|
-
const instructions = RESOURCE_GUIDE_INSTRUCTIONS[type] || definition.description;
|
|
4290
|
+
const instructions = pageGuidance?.instructions || RESOURCE_GUIDE_INSTRUCTIONS[type] || definition.description;
|
|
4259
4291
|
const sources = (guidance.sourceResourceIds || [])
|
|
4260
4292
|
.map((id) => state.resources.find(({ record }) => record.id === id))
|
|
4261
4293
|
.filter(Boolean);
|
|
@@ -4266,7 +4298,8 @@ function resourceGuide(type) {
|
|
|
4266
4298
|
const reviewGuide = reviewPoints.length
|
|
4267
4299
|
? '<div class="guide-review"><span>When reviewing</span><ul>' + reviewPoints.map((point) => '<li>' + esc(point) + '</li>').join("") + '</ul></div>'
|
|
4268
4300
|
: "";
|
|
4269
|
-
|
|
4301
|
+
const output = pageGuidance?.output || RESOURCE_GUIDE_OUTPUTS[type] || 'A validated ' + definition.title + ' record.';
|
|
4302
|
+
return '<section class="page-guide resource-guide-popover" id="resource-guide" role="dialog" aria-label="How to use ' + esc(definition.pluralTitle) + '" hidden><div><span>Do now</span><p>' + esc(instructions) + '</p></div><div><span>Output</span><p>' + esc(output) + '</p></div><div><span>Policy basis</span><p>' + esc(guidance.policyBasis) + '</p>' + sourceLinks + '</div>' + reviewGuide + '</section>';
|
|
4270
4303
|
}
|
|
4271
4304
|
|
|
4272
4305
|
function setupResourceGuide(main) {
|
|
@@ -6486,6 +6519,7 @@ html,body{height:100%;overflow:hidden}.shell{grid-template-columns:248px minmax(
|
|
|
6486
6519
|
.brand{flex:0 0 auto;margin-bottom:18px}.sidebar-nav{--nav-control-width:14px;flex:1;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding-right:1px}.nav-home{margin-bottom:10px}.nav-stage>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0}.nav-stage>.nav-heading:hover{background:rgba(255,255,255,.08);color:#fff}.nav-stage-number{display:grid;place-items:center;width:22px;height:22px;border:1px solid rgba(255,255,255,.26);border-radius:50%;font-size:9.6px}.nav-stage-copy,.nav-stage-copy strong,.nav-stage-copy small{display:block;min-width:0}.nav-stage-copy strong{font-size:12px;line-height:1.25}.nav-stage-copy small{margin-top:2px;color:#aeb6d8;font-size:9.6px;line-height:1.25;font-weight:500}.nav-stage>.nav-items{margin:2px 0 8px 17px;padding:1px 0 5px 12px;border-left:1px solid rgba(255,255,255,.14)}.nav-subgroup>.nav-subheading,.nav-subgroup>.nav-items a{width:100%;display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;padding-right:6px}.nav-subgroup>.nav-subheading{border:0;background:none;padding-top:7px;padding-bottom:4px;padding-left:7px;color:#919bc4;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780;cursor:pointer}.nav-control,.nav-control-slot{justify-self:end;width:var(--nav-control-width);text-align:right}.nav-subgroup>.nav-items{padding:1px 0 4px 3px}.nav-subgroup>.nav-items a{padding-top:6px;padding-bottom:6px;padding-left:8px;font-size:13.2px}.nav-group.open>.nav-heading>.chevron:before,.nav-group.open>.nav-subheading>.chevron:before{transform:rotate(45deg)}.nav-stage.open>.nav-items>.nav-subgroup:not(.open)>.nav-items{display:none}.sidebar-footer{flex:0 0 auto;margin:10px -18px -18px;padding:10px 18px 14px;background:#000024;border-top:1px solid rgba(255,255,255,.14)}.organization-nav{display:grid;grid-template-columns:32px minmax(0,1fr) 12px;gap:9px;align-items:center;padding:8px;border-radius:8px;color:#eef1ff;text-decoration:none}.organization-nav:hover,.organization-nav.current{background:rgba(255,255,255,.11)}.organization-mark{display:grid;place-items:center;width:32px;height:32px;border:1px solid rgba(255,255,255,.25);border-radius:50%;background:rgba(255,255,255,.08);font-size:13.2px;font-weight:800}.organization-nav strong,.organization-nav small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organization-nav strong{font-size:12px}.organization-nav small{margin-top:2px;color:#aeb6d8;font-size:9.6px}.organization-arrow{color:#aeb6d8;font-size:19.2px}.organization-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.organization-links{display:grid}.organization-links a{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-top:1px solid var(--line);text-decoration:none}.organization-links a:first-child{padding-top:0;border-top:0}.organization-links strong,.organization-links small{display:block}.organization-links strong{font-size:13.2px}.organization-links small{margin-top:3px;color:var(--muted);font-size:10.8px;line-height:1.4}.organization-links b{color:var(--accent);font-size:13.2px}.organization-links a:hover strong{color:var(--accent)}
|
|
6487
6520
|
.nav-stage>.nav-items>a.nav-direct{display:grid;grid-template-columns:minmax(0,1fr) var(--nav-control-width);gap:6px;align-items:center;width:100%;padding:6px 6px 6px 7px;font-size:13.2px}
|
|
6488
6521
|
.nav-heading-row{display:grid;grid-template-columns:minmax(0,1fr) 24px;gap:2px;align-items:stretch}.nav-heading-row>.nav-heading{display:grid;grid-template-columns:24px minmax(0,1fr);gap:6px;align-items:center;width:100%;padding:7px 6px;border-radius:7px;color:#d5d9ed;text-align:left;text-transform:none;letter-spacing:0;text-decoration:none}.nav-heading-row>.nav-heading:hover,.nav-heading-row>.nav-heading.current{background:rgba(255,255,255,.1);color:#fff}.nav-toggle{display:grid;place-items:center;width:24px;height:auto;min-height:100%;padding:0;border:0;border-radius:6px;background:none;color:#aeb6d8;cursor:pointer}.nav-toggle:hover{background:rgba(255,255,255,.1);color:#fff}.nav-chevron{display:block;width:12px;height:12px;place-self:center;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;transition:transform .15s}.nav-subheading-row{display:grid;grid-template-columns:minmax(0,1fr) 22px;gap:2px;align-items:center;width:100%;padding:0;border:0;border-radius:6px;background:none;color:#919bc4;cursor:pointer}.nav-subheading-row:hover{background:rgba(255,255,255,.1);color:#fff}.nav-subheading-row>.nav-subheading{display:flex;align-items:center;min-width:0;padding:7px;color:inherit;text-align:left;text-transform:uppercase;letter-spacing:.09em;font-size:9.6px;font-weight:780}.nav-group.open>.nav-heading-row .nav-chevron,.nav-group.open>.nav-subheading-row .nav-chevron{transform:rotate(90deg)}
|
|
6522
|
+
.stage-instructions{margin-top:14px;padding:0 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.stage-instructions>summary{padding:14px 0;cursor:pointer;font-size:12px;font-weight:700}.stage-instructions-body{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:18px;padding:2px 0 18px}.stage-instructions h3{margin:0 0 7px;font-size:12px}.stage-instructions ol{margin:0;padding-left:20px}.stage-instructions li{margin:0 0 6px;color:var(--muted);font-size:11px;line-height:1.5}
|
|
6489
6523
|
.stage-overview-hero{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;align-items:center;padding:26px 28px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stage-overview-hero h2,.group-overview-head h2{font:500 37.2px Georgia,serif;margin:7px 0}.stage-overview-hero>div>p:not(.kicker),.group-overview-head>div>p:not(.kicker){max-width:760px;color:var(--muted);font-size:14.4px;line-height:1.55;margin:0}.stage-progress-card{padding:15px 17px;background:var(--paper);border:1px solid var(--line);border-radius:9px}.stage-progress-card>div:first-child{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}.stage-progress-card>div:first-child>strong{font:500 33.6px Georgia,serif}.stage-progress-card p{color:var(--muted);font-size:10.8px;line-height:1.45;margin:9px 0 0}.badge.neutral{background:#e5e8f2;color:#555e73}.stage-overview-layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:15px;margin-top:15px;align-items:start}.stage-plan ol,.group-plan ol{display:grid;gap:12px;padding-left:20px;margin:0}.stage-plan li,.group-plan li{padding-left:4px;color:var(--ink);font-size:13.2px;line-height:1.5}.stage-groups{min-width:0}.stage-groups>.section-head{margin:4px 0 13px}.stage-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-group-card{position:relative;display:block;min-height:138px;padding:18px 38px 17px 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.stage-group-card:hover{border-color:var(--accent-light)}.stage-group-card h3{font-size:15.6px;margin:0 0 7px}.stage-group-card p{color:var(--muted);font-size:12px;line-height:1.5;margin:0}.stage-group-card small{display:block;color:var(--accent);font-size:9.6px;font-weight:700;margin-top:12px}.stage-group-arrow{position:absolute;right:16px;top:16px;color:var(--accent);font-size:24px}.group-overview-head{display:flex;justify-content:space-between;align-items:end;gap:25px;margin-bottom:15px}.stage-status-link{display:grid;grid-template-columns:auto auto;align-items:center;gap:4px 12px;min-width:155px;padding:12px 14px;background:var(--panel);border:1px solid var(--line);border-radius:9px;text-decoration:none}.stage-status-link>strong{font:500 28.8px Georgia,serif;text-align:right}.stage-status-link>small{grid-column:1/-1;color:var(--muted);font-size:9.6px;text-align:right}.relationship-note{display:grid;grid-template-columns:250px minmax(0,1fr);gap:20px;align-items:center;margin-bottom:15px;padding:17px 20px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.relationship-note h3{font-size:15.6px;margin:5px 0 0}.relationship-note>p{color:var(--muted);font-size:12px;line-height:1.55;margin:0}.relationship-note code{font-size:10.8px}.group-plan{margin-bottom:24px}.group-related-links{display:flex;align-items:center;gap:8px;margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}.group-related-links>span{color:var(--muted);font-size:10.8px;margin-right:auto}.group-destinations>.section-head{margin-bottom:12px}.group-destination-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.group-destination-card{display:grid;grid-template-columns:minmax(0,1fr) 100px;gap:16px;min-height:150px;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;text-decoration:none;box-shadow:0 2px 8px rgba(21,40,33,.025)}.group-destination-card:hover{border-color:var(--accent-light)}.group-destination-card h3{font-size:15.6px;margin:5px 0 7px}.group-destination-card p:not(.kicker){color:var(--muted);font-size:10.8px;line-height:1.5;margin:0}.destination-rollup{align-self:center;text-align:right}.destination-rollup strong,.destination-rollup small{display:block}.destination-rollup strong{font:500 32.4px Georgia,serif}.destination-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:3px}
|
|
6490
6524
|
.collection-review-panel{margin:16px 0 22px}.collection-review-panel.required{border-color:#d8bd78}.collection-review-panel.current{border-color:#b9dac6}.collection-review-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.collection-review-head h3{margin:4px 0 6px}.collection-review-head p:not(.kicker){max-width:900px;margin:0;color:var(--muted);font-size:11px;line-height:1.5}.collection-review-panel details{margin-top:13px;padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-panel summary{cursor:pointer;font-size:11px;font-weight:750}.collection-review-panel ul,.collection-review-checks ul{margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.55}.collection-review-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:13px}.collection-review-result{display:flex;align-items:baseline;gap:8px;margin:0}.collection-review-result strong{font-size:11px}.collection-review-result span{color:var(--muted);font-size:10.4px}.collection-review-checks{margin:13px 0;padding:11px 13px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.collection-review-checks>strong{font-size:11px}.event-dialog-steps.collection-review-checks{margin:15px 0 0;padding:10px;border:0}.collection-review-dialog textarea{box-sizing:border-box;width:100%;resize:vertical}.review-save-status{margin-right:auto;color:var(--muted);font-size:11px}.resource-review-criteria{margin:16px 0 22px;padding:13px 15px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.resource-review-criteria>strong{font-size:11px}.resource-review-criteria ul{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 28px;margin:9px 0 0;padding-left:20px;color:var(--muted);font-size:10.4px;line-height:1.5}.commit-dialog .resource-review-criteria{margin:12px 0;background:var(--surface-soft)}.record-workflow-action{display:grid;grid-template-columns:auto minmax(140px,1fr);gap:7px;align-items:start;min-width:240px;text-decoration:none}.record-workflow-action strong,.record-workflow-action small{display:block}.record-workflow-action strong{font-size:10.4px}.record-workflow-action small{margin-top:2px;color:var(--muted);font-size:9.2px;line-height:1.35}.record-workflow-clear{color:var(--muted);font-size:10px;white-space:nowrap}.workflow-guidance{margin:16px 0 22px}.workflow-guidance .panel-head{align-items:flex-start;margin-bottom:12px}.workflow-guidance .panel-head h3{margin:4px 0}.workflow-guidance .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:11px}.workflow-findings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.workflow-findings>a,.workflow-findings>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;padding:10px 11px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.workflow-findings strong,.workflow-findings small{display:block}.workflow-findings strong{font-size:11px}.workflow-findings small{margin-top:3px;color:var(--muted);font-size:9.6px;line-height:1.4}.workflow-finding-status{min-width:62px;padding:3px 5px;border-radius:99px;background:var(--accent-soft);color:var(--accent);font-size:8.4px;font-weight:750;text-align:center;text-transform:uppercase;letter-spacing:.04em}.workflow-finding-status.overdue,.workflow-finding-status.blocked{background:#f5ded9;color:#8d352c}.workflow-finding-status.ready,.workflow-finding-status.due,.workflow-finding-status.open{background:#f7e9cf;color:#855717}.workflow-finding-status.complete{background:#ddefe5;color:#176143}.workflow-guidance-more{margin:10px 0 0;color:var(--muted);font-size:9.6px}.workflow-guidance-more>summary{cursor:pointer;font-weight:750}.workflow-findings-more{margin-top:9px}.workflow-guidance-more>p{margin:8px 0 0}.context-workflow{display:flex;align-items:center;justify-content:space-between;gap:24px;margin:-8px 0 18px;padding:17px 19px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:10px}.context-workflow h3{font-size:16px;margin:4px 0 5px}.context-workflow p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.5;margin:0;max-width:850px}.context-workflow .button{white-space:nowrap}.policy-lifecycle-note{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(260px,1fr);gap:24px;margin-top:18px}.policy-lifecycle-note h3{margin:5px 0 8px}.policy-lifecycle-note p:not(.kicker),.policy-library-proposals p{color:var(--muted);font-size:11px;line-height:1.55}.policy-library-proposals{display:grid;gap:8px}.policy-library-proposals article{padding:11px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft)}.policy-library-proposals p{margin:5px 0 8px}.policy-activation{display:grid;gap:12px;margin-top:22px}.policy-activation-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.policy-activation-card{padding:17px 18px;border:1px solid #d8bd78;border-radius:10px;background:var(--panel)}.policy-activation-card.active-and-operating{border-color:#b9dac6}.policy-activation-card h3{margin:7px 0 0}.policy-activation-gaps{display:grid;gap:9px;margin-top:13px}.policy-activation-gaps>div{padding-top:8px;border-top:1px solid var(--line)}.policy-activation-gaps small{display:block;margin-bottom:6px;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}.policy-activation-warning{margin:11px 0 0;padding:9px 10px;border-radius:7px;background:#f7e9cf;color:#6d4917;font-size:10.4px;line-height:1.45}.evidence-map{display:grid;gap:12px;margin-top:18px}.evidence-map-head{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 22px;background:var(--accent-soft);border:1px solid #cbd3ff;border-radius:11px}.evidence-map-head>div:first-child{max-width:760px}.evidence-map-head h2{font:500 24px Georgia,serif;margin:5px 0 7px}.evidence-map-head p:not(.kicker){color:var(--muted);font-size:12px;line-height:1.55;margin:0}.evidence-map-actions{display:flex;gap:8px}.evidence-map-card{padding:18px 20px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025)}.evidence-map-card.complete{border-color:#b9dac6}.evidence-map-card-head{display:flex;align-items:start;justify-content:space-between;gap:20px}.evidence-map-card-head h3{font-size:16px;margin:7px 0 0}.evidence-map-card-head>small{color:var(--muted);font-size:10px;text-align:right}.evidence-map-card>p{color:var(--muted);font-size:12px;line-height:1.55;margin:12px 0}.evidence-map-expectation{display:grid;grid-template-columns:120px minmax(0,1fr);gap:12px;padding:8px 0;border-top:1px solid var(--line);font-size:11px;line-height:1.5}.evidence-map-expectation strong{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.evidence-map-expectation code{background:var(--paper);border-radius:4px;padding:2px 5px}.evidence-map-links{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.2fr);gap:20px;margin-top:13px;padding-top:13px;border-top:1px solid var(--line)}.evidence-map-links>div>small{display:block;color:var(--muted);font-size:9.6px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:7px}.evidence-map-references,.evidence-map-sources{display:flex;flex-wrap:wrap;gap:5px}.evidence-map-source{display:flex;align-items:center;gap:7px;padding:7px 9px;background:var(--paper);border:1px solid var(--line);border-radius:7px;font-size:11px;text-decoration:none}.evidence-map-source.complete{border-color:#b9dac6;background:#edf7f1}.evidence-map-source small{color:var(--muted);font-size:9px}.evidence-map-status{padding:9px 11px;background:var(--paper);border-radius:7px}.evidence-map-empty{padding:24px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.evidence-map-empty h3{margin:6px 0}.evidence-map-empty p:not(.kicker){color:var(--muted);font-size:12px;margin:0 0 14px}.stage-pages{margin-top:24px}.stage-page-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stage-page-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:0 2px 8px rgba(21,40,33,.025);transition:border-color .15s,box-shadow .15s}.stage-page-card:hover{border-color:var(--accent-light);box-shadow:0 5px 16px rgba(21,40,33,.07)}.stage-page-card.complete{border-color:#b9dac6}.stage-page-card-head{display:flex;align-items:center;justify-content:space-between;gap:18px}.stage-page-card-head h3{font-size:15.6px;line-height:1.35;margin:4px 0 0}.stage-page-card-head>div>small{display:block;color:var(--accent);font-size:9.6px;font-weight:700}.stage-page-card>p{color:var(--muted);font-size:12px;line-height:1.5;margin:13px 0 0}.stage-page-tasks{position:relative;z-index:2;display:grid;gap:6px;margin-top:13px}.stage-page-tasks>a{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:9px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft);text-decoration:none}.stage-page-tasks>a:hover{border-color:var(--accent-light)}.stage-page-tasks strong,.stage-page-tasks small{display:block}.stage-page-tasks strong{font-size:10.8px}.stage-page-tasks small{margin-top:2px;color:var(--muted);font-size:9.4px;line-height:1.35}.stage-page-tasks-more{color:var(--muted);font-size:9.4px}.stage-page-rollup{display:flex;flex:0 0 104px;flex-direction:column;justify-content:center;text-align:right}.stage-page-rollup strong,.stage-page-rollup small{display:block}.stage-page-rollup strong{font:500 24px Georgia,serif}.stage-page-rollup small{color:var(--muted);font-size:9.6px;line-height:1.35;margin-top:2px}.stage-page-card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto;padding-top:15px}.stage-page-completion-state{color:var(--muted);font-size:10.8px}.stage-page-completion-state.complete{color:#176143;font-weight:700}.stage-page-open{color:var(--accent);font-size:12px;font-weight:700}.work-queue-section{margin-top:28px}.work-queue-section>.section-head{align-items:end}
|
|
6491
6525
|
.stage-page-card-actions{display:flex;align-items:center;gap:10px}.stage-page-card-actions .stage-page-open{white-space:nowrap}.stage-page-open::after{content:"";position:absolute;inset:0;z-index:1;border-radius:10px}.stage-page-open:focus-visible::after{outline:2px solid var(--focus);outline-offset:2px}.stage-page-open>span{position:relative;z-index:2}
|
|
@@ -6669,6 +6703,7 @@ dialog::backdrop{background:rgba(0,0,24,.62)}
|
|
|
6669
6703
|
@media(max-width:760px){.record-table td:has(.relation){min-width:0}.record-table td[data-label]>.relation{grid-column:2}}
|
|
6670
6704
|
@media(max-width:760px){.diagnostics>div{grid-template-columns:max-content minmax(0,1fr)}}
|
|
6671
6705
|
.workflow-guidance .workflow-status{margin:4px 0 12px;color:var(--muted);font-size:10px}.workflow-completion-requirements{display:inline;appearance:none;-webkit-appearance:none;white-space:nowrap;padding:0;border:0;background:none;color:var(--accent);font:inherit;font-weight:750;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.workflow-completion-requirements:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.completion-requirements-dialog ul{margin:14px 0 0;padding-left:20px;color:var(--muted);font-size:12px;line-height:1.55}
|
|
6706
|
+
.control-next-steps{margin:8px 0 9px;padding-left:18px;font-size:12px;line-height:1.5}.control-next-steps li+li{margin-top:7px}.detail-control aside .detail-workflow-panel{max-height:none;overflow:visible}
|
|
6672
6707
|
.record-completion-state{background:var(--surface-soft)}.record-completion-state h3{margin:4px 0 5px;font-size:16.8px}.record-completion-state p:last-child{margin:0;color:var(--muted);font-size:11px;line-height:1.45}
|
|
6673
6708
|
`;
|
|
6674
6709
|
|
package/src/workflow.js
CHANGED
|
@@ -1128,6 +1128,7 @@ function normalizeFinding(code, item, context) {
|
|
|
1128
1128
|
severity: findingSeverity(state, context.assessment),
|
|
1129
1129
|
title: item.title,
|
|
1130
1130
|
message: item.message,
|
|
1131
|
+
...(item.nextSteps?.length ? { nextSteps: item.nextSteps } : {}),
|
|
1131
1132
|
...(item.resourceId ? { resourceId: item.resourceId } : {}),
|
|
1132
1133
|
...(item.createResourceType ? { createResourceType: item.createResourceType } : {}),
|
|
1133
1134
|
...(item.sourceResourceIds ? { sourceResourceIds: item.sourceResourceIds } : {}),
|