filegrc 0.16.4 → 0.16.6
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/action-context.js +70 -0
- package/src/agent.js +12 -2
- package/src/cli.js +15 -9
- package/src/program-path.js +2 -2
- package/src/program-readiness.js +131 -46
- package/src/reconciliation.js +1 -0
- package/src/retention-schedule-approval.js +5 -5
- package/src/server.js +4 -3
- package/src/web.js +20 -5
- package/src/workflow.js +12 -4
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Keep a short, source-backed inventory beside a derived next action. A link
|
|
2
|
+
// means a record exists; it never proves that the recorded process operated.
|
|
3
|
+
export function buildActionContext(loaded, item) {
|
|
4
|
+
const records = loaded.resources;
|
|
5
|
+
const byId = new Map(records.map((record) => [record.id, record]));
|
|
6
|
+
const subjectId = item.subject?.id || item.resourceId;
|
|
7
|
+
const subject = subjectId ? byId.get(subjectId) : null;
|
|
8
|
+
const found = new Map();
|
|
9
|
+
const add = (id, relationship) => {
|
|
10
|
+
const record = byId.get(id);
|
|
11
|
+
if (!record || found.has(id)) return;
|
|
12
|
+
found.set(id, {
|
|
13
|
+
type: record.type,
|
|
14
|
+
id: record.id,
|
|
15
|
+
title: record.title,
|
|
16
|
+
status: record.status ?? null,
|
|
17
|
+
relationship
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
if (subject) add(subject.id, "subject");
|
|
21
|
+
if (item.source?.id) add(item.source.id, "source");
|
|
22
|
+
for (const id of item.sourceResourceIds || []) add(id, "source");
|
|
23
|
+
for (const dependency of item.dependencies || []) if (dependency.id) add(dependency.id, "prerequisite");
|
|
24
|
+
const matchingType = item.createResourceType || (!subject && (item.subject?.type || item.resourceType));
|
|
25
|
+
const sameType = matchingType
|
|
26
|
+
? records.filter((record) => record.type === matchingType)
|
|
27
|
+
: [];
|
|
28
|
+
if (subject) {
|
|
29
|
+
const fields = { ...loaded.model.commonFields, ...loaded.model.resources[subject.type]?.fields };
|
|
30
|
+
for (const [name, definition] of Object.entries(fields)) {
|
|
31
|
+
if (!definition?.relation) continue;
|
|
32
|
+
const ids = Array.isArray(subject[name]) ? subject[name] : [subject[name]];
|
|
33
|
+
for (const id of ids) if (typeof id === "string") add(id, name);
|
|
34
|
+
}
|
|
35
|
+
for (const record of records) {
|
|
36
|
+
if (record.id === subject.id) continue;
|
|
37
|
+
const incomingFields = { ...loaded.model.commonFields, ...loaded.model.resources[record.type]?.fields };
|
|
38
|
+
for (const [name, definition] of Object.entries(incomingFields)) {
|
|
39
|
+
if (!definition?.relation) continue;
|
|
40
|
+
const ids = Array.isArray(record[name]) ? record[name] : [record[name]];
|
|
41
|
+
if (ids.includes(subject.id)) add(record.id, name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const record of sameType) add(record.id, "same-type-candidate");
|
|
46
|
+
const existing = [...found.values()];
|
|
47
|
+
const unmetChecks = item.unmetChecks || Object.entries(item.checks || {})
|
|
48
|
+
.filter(([, passed]) => passed === false)
|
|
49
|
+
.map(([name]) => name);
|
|
50
|
+
return {
|
|
51
|
+
workPhase: actionPhase(item),
|
|
52
|
+
operation: item.createResourceType
|
|
53
|
+
? sameType.length ? "inspect-before-create" : "check-before-create"
|
|
54
|
+
: subject ? "inspect-existing" : sameType.length ? "inspect-before-create" : "check-before-create",
|
|
55
|
+
unmetChecks,
|
|
56
|
+
existing: existing.slice(0, 20),
|
|
57
|
+
existingCount: existing.length,
|
|
58
|
+
existingTruncated: existing.length > 20,
|
|
59
|
+
...(matchingType ? { sameTypeCount: sameType.length } : {}),
|
|
60
|
+
operationProof: "not-inferred-from-links",
|
|
61
|
+
reminder: "Related records show current design or recorded work. Verify actual operation before claiming it occurred."
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function actionPhase(item) {
|
|
66
|
+
if (item.assessment === "period-health" || ["run", "operation"].includes(item.stage)) return "operation";
|
|
67
|
+
if (["audit-readiness", "delivery-readiness", "audit-closure"].includes(item.assessment) || item.stage === "audit") return "audit";
|
|
68
|
+
if (item.assessment === "program-configuration" || ["scope", "policies", "controls"].includes(item.stage)) return "setup";
|
|
69
|
+
return "record";
|
|
70
|
+
}
|
package/src/agent.js
CHANGED
|
@@ -74,6 +74,9 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
74
74
|
const location = definition.singleton
|
|
75
75
|
? `data/${definition.singleton}`
|
|
76
76
|
: `data/${definition.collection}/${(definition.recordPath ?? "{id}.json").replaceAll("{id}", options.id || "{id}")}`;
|
|
77
|
+
const existingRecords = loaded.resources
|
|
78
|
+
.filter((item) => item.type === type)
|
|
79
|
+
.sort((left, right) => `${left.title}:${left.id}`.localeCompare(`${right.title}:${right.id}`));
|
|
77
80
|
|
|
78
81
|
return {
|
|
79
82
|
type,
|
|
@@ -104,6 +107,13 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
104
107
|
: null
|
|
105
108
|
},
|
|
106
109
|
location,
|
|
110
|
+
existingRecordCount: existingRecords.length,
|
|
111
|
+
existingRecords: existingRecords.slice(0, 25).map(({ id, title, status }) => ({
|
|
112
|
+
id,
|
|
113
|
+
title,
|
|
114
|
+
status: status ?? null
|
|
115
|
+
})),
|
|
116
|
+
existingRecordsTruncated: existingRecords.length > 25,
|
|
107
117
|
singleton: Boolean(definition.singleton),
|
|
108
118
|
requiredAtCreation,
|
|
109
119
|
conditionalRequirements,
|
|
@@ -113,8 +123,8 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
113
123
|
workflow: [
|
|
114
124
|
"Inspect existing records and relation candidates before writing.",
|
|
115
125
|
definition.singleton
|
|
116
|
-
? "Open the existing singleton record
|
|
117
|
-
: "Create a scaffold, then replace every null value and empty required array with facts from an authoritative source.",
|
|
126
|
+
? "Open the existing singleton record and update only gaps supported by facts from an authoritative source."
|
|
127
|
+
: "Reuse and update a suitable existing record. Create a scaffold only when no record covers the work, then replace every null value and empty required array with facts from an authoritative source.",
|
|
118
128
|
recommendedMarkdown.length
|
|
119
129
|
? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
|
|
120
130
|
: "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { ACTIVE_MODEL_VERSION, loadModel, SUPPORTED_MODEL_VERSIONS } from "../model/index.js";
|
|
5
5
|
import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
|
|
6
|
+
import { buildActionContext } from "./action-context.js";
|
|
6
7
|
import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
7
8
|
import { saveAuditPopulation, scaffoldAuditPopulationCorrection } from "./audit-populations.js";
|
|
8
9
|
import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
|
|
@@ -295,7 +296,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
295
296
|
programId
|
|
296
297
|
});
|
|
297
298
|
const result = buildProgramPathResult(loaded.model, readiness, auditReadiness);
|
|
298
|
-
const output = selectProgramPathOutput(result, flags);
|
|
299
|
+
const output = selectProgramPathOutput(result, flags, loaded);
|
|
299
300
|
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
300
301
|
else printProgramPathOutput(output, flags);
|
|
301
302
|
return output;
|
|
@@ -935,7 +936,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
935
936
|
const output = flags.workflow
|
|
936
937
|
? {
|
|
937
938
|
record,
|
|
938
|
-
workflow: workflowForResource(await assessWorkflow(loaded), record.type, record.id)
|
|
939
|
+
workflow: workflowForResource(await assessWorkflow(loaded), record.type, record.id, loaded)
|
|
939
940
|
}
|
|
940
941
|
: record;
|
|
941
942
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -1773,6 +1774,9 @@ function printAgentGuide(result) {
|
|
|
1773
1774
|
console.log(`Policy basis: ${result.policyBasis}`);
|
|
1774
1775
|
console.log(`Timing: ${result.cadence}`);
|
|
1775
1776
|
console.log(`JSON: ${result.location}`);
|
|
1777
|
+
console.log(`Existing ${result.pluralTitle}: ${result.existingRecordCount}`);
|
|
1778
|
+
for (const record of result.existingRecords) console.log(`- ${record.id}\t${record.title}\t${record.status || ""}`);
|
|
1779
|
+
if (result.existingRecordsTruncated) console.log("- More records exist. Run filegrc list for the complete set.");
|
|
1776
1780
|
if (result.reviewRequirements.collectionReview) {
|
|
1777
1781
|
const review = result.reviewRequirements.collectionReview;
|
|
1778
1782
|
console.log(`\nCollection review: ${review.title} (${review.status}, ${review.recordCount} ${review.recordCount === 1 ? "record" : "records"})`);
|
|
@@ -1830,7 +1834,7 @@ function buildProgramPathResult(model, readiness, auditReadiness) {
|
|
|
1830
1834
|
const readinessById = new Map(readiness.stages.map((stage) => [stage.id, stage]));
|
|
1831
1835
|
const stages = buildAgentProgramPath(model).map((stage) => {
|
|
1832
1836
|
if (stage.id === "audit") {
|
|
1833
|
-
const auditAction = auditReadiness
|
|
1837
|
+
const auditAction = !auditReadiness || auditReadiness.status === "not-started"
|
|
1834
1838
|
? {
|
|
1835
1839
|
id: "create-audit",
|
|
1836
1840
|
status: "action",
|
|
@@ -1924,11 +1928,11 @@ function printProgramPath(result) {
|
|
|
1924
1928
|
}
|
|
1925
1929
|
}
|
|
1926
1930
|
|
|
1927
|
-
function selectProgramPathOutput(result, flags) {
|
|
1931
|
+
function selectProgramPathOutput(result, flags, loaded) {
|
|
1928
1932
|
const modes = ["summary", "next", "current"].filter((name) => flags[name]);
|
|
1929
1933
|
if (modes.length > 1) throw new Error("Use only one of --summary, --next, or --current.");
|
|
1930
1934
|
if (flags.summary) return summarizeProgramPath(result);
|
|
1931
|
-
if (flags.next) return nextProgramPath(result);
|
|
1935
|
+
if (flags.next) return nextProgramPath(result, loaded);
|
|
1932
1936
|
if (flags.current) {
|
|
1933
1937
|
const stage = result.stages.find(({ id }) => id === result.currentStep.id);
|
|
1934
1938
|
return { ...result, stages: stage ? [stage] : [] };
|
|
@@ -1957,7 +1961,7 @@ function summarizeProgramPath(result) {
|
|
|
1957
1961
|
};
|
|
1958
1962
|
}
|
|
1959
1963
|
|
|
1960
|
-
function nextProgramPath(result) {
|
|
1964
|
+
function nextProgramPath(result, loaded) {
|
|
1961
1965
|
const stage = result.stages.find(({ id }) => id === result.currentStep.id);
|
|
1962
1966
|
const nextAction = stage?.nextActions[0];
|
|
1963
1967
|
return {
|
|
@@ -1975,7 +1979,9 @@ function nextProgramPath(result) {
|
|
|
1975
1979
|
title: stage.title,
|
|
1976
1980
|
status: stage.status,
|
|
1977
1981
|
summary: stage.summary,
|
|
1978
|
-
nextAction:
|
|
1982
|
+
nextAction: nextAction
|
|
1983
|
+
? { ...summarizePathAction(nextAction), context: buildActionContext(loaded, { ...nextAction, stage: stage.id }) }
|
|
1984
|
+
: null,
|
|
1979
1985
|
commands: nextActionCommands(stage, nextAction)
|
|
1980
1986
|
} : null
|
|
1981
1987
|
};
|
|
@@ -2001,11 +2007,11 @@ function nextActionCommands(stage, action) {
|
|
|
2001
2007
|
const commands = [`npx filegrc guide ${resourceType} --json`];
|
|
2002
2008
|
if (action.resourceId) {
|
|
2003
2009
|
const resourceId = shellArgument(action.resourceId);
|
|
2004
|
-
commands.push(`npx filegrc get ${resourceId} --mutation`);
|
|
2010
|
+
commands.push(`npx filegrc get ${resourceId} --mutation > MUTATION.json`);
|
|
2005
2011
|
commands.push(`npx filegrc update ${resourceType} ${resourceId} MUTATION.json --json`);
|
|
2006
2012
|
} else {
|
|
2007
2013
|
commands.push(`npx filegrc list ${resourceType} --json`);
|
|
2008
|
-
commands.push(`npx filegrc scaffold ${resourceType} --title "NAME"`);
|
|
2014
|
+
commands.push(`npx filegrc scaffold ${resourceType} --title "NAME" > MUTATION.json`);
|
|
2009
2015
|
commands.push("npx filegrc create MUTATION.json --json");
|
|
2010
2016
|
}
|
|
2011
2017
|
return commands;
|
package/src/program-path.js
CHANGED
|
@@ -17,7 +17,7 @@ export const RESOURCE_INSTRUCTIONS = {
|
|
|
17
17
|
"reporting-route-set": "Replace the reporting-route placeholders with real normal and fallback channels, name the responsible role, and commit the proposal.",
|
|
18
18
|
policy: "Replace Policy placeholders with actual commitments, then have a separate approver approve the exact Markdown revision.",
|
|
19
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
|
|
20
|
+
control: "Open this Control’s next steps and inspect linked Policy, Obligations, Components, and operating records. Reuse existing workflows; fill only the implementation gap, and mark it Implemented when it works. Record actual operation separately.",
|
|
21
21
|
"complementary-control": "Record each real customer or carved-out provider dependency, or confirm that the current scope has none.",
|
|
22
22
|
evidence: "Record a real artifact or approved reference, link its source and supported work, then have another person verify it.",
|
|
23
23
|
"risk-assessment": "Assess the in-scope service, record conclusions, and obtain an independent review.",
|
|
@@ -118,7 +118,7 @@ export const RESOURCE_PAGE_SUMMARIES = {
|
|
|
118
118
|
"retention-schedule-item": "Review each structured retention rule.",
|
|
119
119
|
policy: "Tailor the starter Policy and have someone other than its owner approve it.",
|
|
120
120
|
document: "Adapt and approve plans.",
|
|
121
|
-
control: "
|
|
121
|
+
control: "Check existing Policy, obligations, and records, then fill each Control’s implementation gap.",
|
|
122
122
|
"complementary-control": "Record customer or provider responsibilities, or confirm there are none.",
|
|
123
123
|
audit: "Record the CPA engagement and scope.",
|
|
124
124
|
"audit-request": "Track fieldwork requests and responses.",
|
package/src/program-readiness.js
CHANGED
|
@@ -470,7 +470,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
470
470
|
)));
|
|
471
471
|
items.push(item(
|
|
472
472
|
"commitments",
|
|
473
|
-
scope.systems.length
|
|
473
|
+
!scope.systems.length ? "later" : uncoveredSystems.length === 0 && unscopedCommitments.length === 0 ? "complete" : "action",
|
|
474
474
|
"Record service commitments and system requirements",
|
|
475
475
|
scope.systems.length
|
|
476
476
|
? `${completeCommitments.length} complete active ${completeCommitments.length === 1 ? "commitment covers" : "commitments cover"} ${scope.systems.length - uncoveredSystems.length} of ${scope.systems.length} in-scope systems.${unscopedCommitments.length ? ` ${unscopedCommitments.length} Commitment${unscopedCommitments.length === 1 ? " has" : "s have"} no System scope and must be reviewed explicitly.` : ""}`
|
|
@@ -711,14 +711,23 @@ function reportingRouteItem(records, byId, asOf, timezone = "UTC") {
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
export function collectionReviewReadinessItem(assessment) {
|
|
714
|
+
const firstIncomplete = assessment.incompleteRecordProposals?.[0]
|
|
715
|
+
|| assessment.recordProposals?.find(({ complete }) => !complete);
|
|
716
|
+
const needsFirstRecord = assessment.recordCount === 0
|
|
717
|
+
&& !(assessment.configuration.decisions || []).some((decision) => ["zero-population", "externally-managed"].includes(decision));
|
|
718
|
+
const proposalAction = !assessment.complete && firstIncomplete;
|
|
719
|
+
const pendingRetentionRow = assessment.resourceType === "retention-schedule-item"
|
|
720
|
+
? assessment.records?.find(({ status }) => status === "planned")
|
|
721
|
+
: null;
|
|
722
|
+
const approvalAction = !proposalAction && !pendingRetentionRow
|
|
723
|
+
? collectionApprovalIssueAction(assessment)
|
|
724
|
+
: null;
|
|
714
725
|
const proposalUnits = (assessment.recordProposals || []).map((proposal) => ({
|
|
715
726
|
id: `${assessment.resourceType}-proposal-${proposal.resourceId}`,
|
|
716
727
|
status: proposal.complete ? "complete" : "action",
|
|
717
728
|
title: `${proposal.title} proposal`
|
|
718
729
|
}));
|
|
719
730
|
const proposalBatchComplete = proposalUnits.every(({ status }) => status === "complete");
|
|
720
|
-
const scheduleApprovalBlocked = assessment.resourceType === "retention-schedule-item"
|
|
721
|
-
&& proposalUnits.some(({ status }) => status !== "complete");
|
|
722
731
|
const batchProgressUnits = assessment.resourceType !== "retention-schedule-item" && proposalUnits.length
|
|
723
732
|
? [
|
|
724
733
|
{
|
|
@@ -737,24 +746,100 @@ export function collectionReviewReadinessItem(assessment) {
|
|
|
737
746
|
: null;
|
|
738
747
|
return item(
|
|
739
748
|
`collection-review-${assessment.resourceType}`,
|
|
740
|
-
assessment.complete ? "complete" :
|
|
741
|
-
|
|
742
|
-
? `
|
|
743
|
-
:
|
|
744
|
-
|
|
745
|
-
|
|
749
|
+
assessment.complete ? "complete" : pendingRetentionRow && !proposalAction ? "blocked" : "action",
|
|
750
|
+
proposalAction
|
|
751
|
+
? `Complete ${firstIncomplete.title} proposal`
|
|
752
|
+
: needsFirstRecord
|
|
753
|
+
? `Add a record for ${assessment.configuration.title.toLowerCase()}`
|
|
754
|
+
: approvalAction
|
|
755
|
+
? approvalAction.title
|
|
756
|
+
: assessment.status === "stale"
|
|
757
|
+
? `Review ${assessment.configuration.title.toLowerCase()} again`
|
|
758
|
+
: `Review ${assessment.configuration.title.toLowerCase()}`,
|
|
759
|
+
proposalAction
|
|
760
|
+
? `Complete ${firstIncomplete.title} before reviewing ${assessment.configuration.title.toLowerCase()}.`
|
|
761
|
+
: assessment.message,
|
|
762
|
+
proposalAction
|
|
763
|
+
? { id: firstIncomplete.resourceId, type: assessment.resourceType }
|
|
764
|
+
: needsFirstRecord ? { type: assessment.resourceType }
|
|
765
|
+
: approvalAction ? { type: approvalAction.resourceType, ...(approvalAction.resourceId ? { id: approvalAction.resourceId } : {}) }
|
|
766
|
+
: assessment.review || { type: assessment.resourceType },
|
|
746
767
|
{
|
|
747
|
-
resourceType: assessment.resourceType,
|
|
768
|
+
resourceType: approvalAction?.resourceType || assessment.resourceType,
|
|
748
769
|
reviewPoints: assessment.configuration.reviewPoints,
|
|
770
|
+
...(approvalAction?.createResource ? { createResource: true } : {}),
|
|
771
|
+
...(pendingRetentionRow && !proposalAction ? { unresolvedAssignments: [{
|
|
772
|
+
resourceType: "retention-schedule-item",
|
|
773
|
+
resourceId: pendingRetentionRow.id,
|
|
774
|
+
reasons: ["Complete or retire the planned retention row before approving the schedule."]
|
|
775
|
+
}] } : {}),
|
|
749
776
|
...(batchProgressUnits ? { progressUnits: batchProgressUnits } : {}),
|
|
750
|
-
commands:
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
777
|
+
commands: proposalAction
|
|
778
|
+
? [
|
|
779
|
+
`npx filegrc guide ${assessment.resourceType} --json`,
|
|
780
|
+
`npx filegrc get ${shellArgument(firstIncomplete.resourceId)} --mutation > MUTATION.json`,
|
|
781
|
+
`npx filegrc update ${assessment.resourceType} ${shellArgument(firstIncomplete.resourceId)} MUTATION.json --json`
|
|
782
|
+
]
|
|
783
|
+
: needsFirstRecord
|
|
784
|
+
? [
|
|
785
|
+
`npx filegrc guide ${assessment.resourceType} --json`,
|
|
786
|
+
`npx filegrc scaffold ${assessment.resourceType} --title "NAME" > MUTATION.json`,
|
|
787
|
+
"npx filegrc create MUTATION.json --json"
|
|
788
|
+
]
|
|
789
|
+
: approvalAction
|
|
790
|
+
? approvalAction.commands
|
|
791
|
+
: [
|
|
792
|
+
`npx filegrc review-collection ${assessment.resourceType} --scaffold > REVIEW.json`,
|
|
793
|
+
`npx filegrc review-collection ${assessment.resourceType} REVIEW.json --preview --json`,
|
|
794
|
+
`npx filegrc review-collection ${assessment.resourceType} REVIEW.json --yes --json`
|
|
795
|
+
]
|
|
754
796
|
}
|
|
755
797
|
);
|
|
756
798
|
}
|
|
757
799
|
|
|
800
|
+
function collectionApprovalIssueAction(assessment) {
|
|
801
|
+
const issue = assessment.approvalIssues?.[0];
|
|
802
|
+
if (!issue || issue.code === "invalid-retention-schedule-approval-binding") return null;
|
|
803
|
+
const rows = assessment.records || [];
|
|
804
|
+
const documentId = issue.resourceId || rows.find(({ scheduleDocumentId }) => scheduleDocumentId)?.scheduleDocumentId;
|
|
805
|
+
if (issue.code.includes("retention-schedule-document")) {
|
|
806
|
+
const resourceId = issue.code === "missing-retention-schedule-document" || issue.code === "multiple-retention-schedule-documents"
|
|
807
|
+
? null : documentId;
|
|
808
|
+
return {
|
|
809
|
+
title: issue.code === "missing-retention-schedule-document" ? "Add the Data Retention Schedule document"
|
|
810
|
+
: issue.code === "multiple-retention-schedule-documents" ? "Resolve duplicate Data Retention Schedule documents"
|
|
811
|
+
: "Complete the Data Retention Schedule document",
|
|
812
|
+
resourceType: "document",
|
|
813
|
+
resourceId,
|
|
814
|
+
createResource: issue.code === "missing-retention-schedule-document",
|
|
815
|
+
commands: resourceId
|
|
816
|
+
? ["npx filegrc guide document --json", `npx filegrc get ${shellArgument(resourceId)} --mutation > MUTATION.json`, `npx filegrc update document ${shellArgument(resourceId)} MUTATION.json --json`]
|
|
817
|
+
: issue.code === "missing-retention-schedule-document"
|
|
818
|
+
? ["npx filegrc guide document --json", 'npx filegrc scaffold document --title "Data Retention Schedule" > MUTATION.json', "npx filegrc create MUTATION.json --json"]
|
|
819
|
+
: ["npx filegrc guide document --json", "npx filegrc list document --workflow --json"]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (["retention-row-wrong-schedule-document", "incomplete-retention-schedule-row"].includes(issue.code)) {
|
|
823
|
+
const row = rows.find(({ id }) => id === issue.resourceId);
|
|
824
|
+
if (row) return {
|
|
825
|
+
title: `Complete ${row.title}`,
|
|
826
|
+
resourceType: "retention-schedule-item",
|
|
827
|
+
resourceId: row.id,
|
|
828
|
+
commands: ["npx filegrc guide retention-schedule-item --json", `npx filegrc get ${shellArgument(row.id)} --mutation > MUTATION.json`, `npx filegrc update retention-schedule-item ${shellArgument(row.id)} MUTATION.json --json`]
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
if (issue.code === "unreviewed-similar-information-types") return {
|
|
832
|
+
title: "Review similar Information Types",
|
|
833
|
+
resourceType: "information-type",
|
|
834
|
+
commands: ["npx filegrc list information-type --workflow --json", "npx filegrc review-collection information-type --scaffold > REVIEW.json", "npx filegrc review-collection information-type REVIEW.json --preview --json", "npx filegrc review-collection information-type REVIEW.json --yes --json"]
|
|
835
|
+
};
|
|
836
|
+
return {
|
|
837
|
+
title: "Complete retention schedule coverage",
|
|
838
|
+
resourceType: "retention-schedule-item",
|
|
839
|
+
commands: ["npx filegrc guide retention-schedule-item --json", "npx filegrc list retention-schedule-item --workflow --json", 'npx filegrc scaffold retention-schedule-item --title "RETENTION RULE" > MUTATION.json', "npx filegrc create MUTATION.json --json"]
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
758
843
|
function requiredAppointmentsItem(records, model) {
|
|
759
844
|
const assessments = assessRequiredAppointments(records, model);
|
|
760
845
|
const incomplete = assessments.filter(({ requiredness, state }) => (
|
|
@@ -1953,41 +2038,41 @@ function controlCheckLabel(name) {
|
|
|
1953
2038
|
})[name] || name;
|
|
1954
2039
|
}
|
|
1955
2040
|
|
|
1956
|
-
const
|
|
1957
|
-
"control-security-governance": "
|
|
1958
|
-
"control-policy-management": "
|
|
1959
|
-
"control-security-communication": "
|
|
1960
|
-
"control-workforce-expectations": "
|
|
1961
|
-
"control-security-training": "
|
|
1962
|
-
"control-risk-assessment": "
|
|
1963
|
-
"control-monitoring-remediation": "
|
|
1964
|
-
"control-access-authorization": "
|
|
1965
|
-
"control-strong-authentication": "
|
|
1966
|
-
"control-access-review-offboarding": "
|
|
1967
|
-
"control-physical-workspace-security": "
|
|
1968
|
-
"control-data-classification-inventory": "
|
|
1969
|
-
"control-encryption-transmission": "
|
|
1970
|
-
"control-data-retention-disposal": "
|
|
1971
|
-
"control-inventory-configuration": "
|
|
1972
|
-
"control-endpoint-protection": "
|
|
1973
|
-
"control-network-security": "
|
|
1974
|
-
"control-change-management": "
|
|
1975
|
-
"control-vulnerability-management": "
|
|
1976
|
-
"control-penetration-testing": "Decide whether independent testing is needed
|
|
1977
|
-
"control-logging-monitoring": "
|
|
1978
|
-
"control-incident-response": "
|
|
1979
|
-
"control-incident-exercise": "
|
|
1980
|
-
"control-backup-restoration": "
|
|
1981
|
-
"control-continuity-exercise": "
|
|
1982
|
-
"control-vendor-due-diligence": "
|
|
1983
|
-
"control-vendor-monitoring": "
|
|
1984
|
-
"control-security-exceptions": "
|
|
2041
|
+
const starterControlChecks = {
|
|
2042
|
+
"control-security-governance": "Open the security review calendar and attendee roles. Check a reviewer separate from owners and operators examines risks, incidents, findings, and overdue work on the approved schedule.",
|
|
2043
|
+
"control-policy-management": "Open the Control inventory and a recently revised Policy. Check owners are named, the approver is separate, and the change went through review before approval.",
|
|
2044
|
+
"control-security-communication": "Trace how a security notice or Policy change reaches staff and affected outside parties. Check that the approved reporting routes can be used.",
|
|
2045
|
+
"control-workforce-expectations": "Review onboarding for roles with sensitive access. Check that required screening, agreements, and Policy acknowledgements happen before access.",
|
|
2046
|
+
"control-security-training": "Compare the worker roster with training assignments. Check starters and role changes get training on time, with repeats tied to current content and the approved schedule.",
|
|
2047
|
+
"control-risk-assessment": "Open the risk assessment against in-scope Systems and Vendors. Check threats, fraud, dependencies, and changes have owners and high risks get the required review.",
|
|
2048
|
+
"control-monitoring-remediation": "Open the latest Control review, or schedule the first. Check incidents, test results, Exceptions, and overdue work, with owners and verified closure for gaps.",
|
|
2049
|
+
"control-access-authorization": "Open access lists for in-scope Systems and Vendor tools. Check that accounts use unique identities and only needed permissions, and new grants require a business reason and approval.",
|
|
2050
|
+
"control-strong-authentication": "Open the System and Vendor lists. For each tool used for production, source code, email, identity, or sensitive data, check MFA, unique accounts, default credentials, and admin roles.",
|
|
2051
|
+
"control-access-review-offboarding": "Export access lists from key Systems and Vendor tools. Check privileged and other access reviews meet their approved schedules, and departures lose access within the required window.",
|
|
2052
|
+
"control-physical-workspace-security": "Walk through actual work areas and visitor access. Check who can enter nonpublic spaces and how devices, papers, screens, and conversations are protected.",
|
|
2053
|
+
"control-data-classification-inventory": "List important data stores in Systems and Vendor tools. Check each has an owner, classification, approved purpose, and recorded location.",
|
|
2054
|
+
"control-encryption-transmission": "For Systems, Vendor tools, and devices holding Confidential or Restricted data, check encryption at rest and in transit and who can manage the keys.",
|
|
2055
|
+
"control-data-retention-disposal": "Take each class in the retention schedule and locate its live, backup, and vendor-held copies. Check deletion methods and timing against the approved rule.",
|
|
2056
|
+
"control-inventory-configuration": "Compare actual services, devices, software, service accounts, Vendors, and data stores with the inventory. Check owners, secure settings, and unsupported assets.",
|
|
2057
|
+
"control-endpoint-protection": "Review devices allowed into company Systems. Check encryption, screen lock, updates, and malware protection against the approved settings.",
|
|
2058
|
+
"control-network-security": "Open production network and remote-access rules. Check each path has a business reason, approved authentication, and separation; identify rules due for review.",
|
|
2059
|
+
"control-change-management": "Follow a software or infrastructure change from request to deployment. Check the path includes risk review, testing, approval, and a way to recover.",
|
|
2060
|
+
"control-vulnerability-management": "List internet-facing and important Systems. Check each has vulnerability scan coverage or another approved check, and findings have owners and target dates.",
|
|
2061
|
+
"control-penetration-testing": "Review the service's exposure, major changes, and customer commitments. Decide whether independent testing is needed and, if so, set its scope and cadence.",
|
|
2062
|
+
"control-logging-monitoring": "For each important System, open security logs, alert rules, and service-health checks where needed. Check retention and whether test alerts reach an owner.",
|
|
2063
|
+
"control-incident-response": "Walk through how a worker reports an incident. Check who triages, escalates, contains, communicates, and closes it using the approved plan.",
|
|
2064
|
+
"control-incident-exercise": "Send a test alert through a representative incident path. Check acknowledgement, escalation, and fallback, then record participants, failures, and follow-up.",
|
|
2065
|
+
"control-backup-restoration": "For each important System, inspect backup or alternate recovery settings and failure alerts. Run or review a restore test against its recovery needs.",
|
|
2066
|
+
"control-continuity-exercise": "Review recovery priorities, contacts, and owners. Check the plan covers loss of an important System or provider and has an exercise date.",
|
|
2067
|
+
"control-vendor-due-diligence": "Open the Vendor list. For each with sensitive data or a material service role, check its security review and contract terms; review older Vendors already in use.",
|
|
2068
|
+
"control-vendor-monitoring": "Open critical and high-risk Vendors. Check each has a current review, an owner for open issues, and timely reassessment after a material change or incident.",
|
|
2069
|
+
"control-security-exceptions": "Review departures from security rules. Check each has a reason, owner, risk assessment, compensating measure, approval, and expiry or review date."
|
|
1985
2070
|
};
|
|
1986
2071
|
|
|
1987
2072
|
export function controlImplementationSteps(control, checks, options = {}) {
|
|
1988
2073
|
const steps = [];
|
|
1989
|
-
if (!checks.implemented) steps.push(
|
|
1990
|
-
||
|
|
2074
|
+
if (!checks.implemented) steps.push(starterControlChecks[control.id]
|
|
2075
|
+
|| "Inspect the systems, people, vendors, or work this Control covers. Compare current practice with its statement and fix any gaps.");
|
|
1991
2076
|
const record = [];
|
|
1992
2077
|
if (!checks.owner) record.push("choose an owner");
|
|
1993
2078
|
if (!checks.scope) record.push("select the Systems this covers");
|
|
@@ -2007,7 +2092,7 @@ export function controlImplementationSteps(control, checks, options = {}) {
|
|
|
2007
2092
|
? `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
2093
|
: null;
|
|
2009
2094
|
const queueStep = checks.workQueue === false
|
|
2010
|
-
? "
|
|
2095
|
+
? "Check the linked Obligations. Enable one with the owner and schedule for this Control's recurring or event work, or add one if none covers it."
|
|
2011
2096
|
: null;
|
|
2012
2097
|
if (!checks.implemented) {
|
|
2013
2098
|
if (sourceStep || queueStep) steps.push([sourceStep, queueStep].filter(Boolean).join(" "));
|
package/src/reconciliation.js
CHANGED
|
@@ -769,6 +769,7 @@ function validateMergeIdentities(index, model) {
|
|
|
769
769
|
const identities = new Map();
|
|
770
770
|
for (const parent of parents) {
|
|
771
771
|
const state = recordStateAtCommit(index, model, parent);
|
|
772
|
+
if (!state) continue;
|
|
772
773
|
let snapshot = snapshots.get(state);
|
|
773
774
|
if (!snapshot) {
|
|
774
775
|
snapshot = state.snapshot || snapshotRecordState(state);
|
|
@@ -43,7 +43,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
43
43
|
|| !substantiveMarkdown(scheduleContent)
|
|
44
44
|
|| openPlaceholderCount(scheduleContent) > 0)
|
|
45
45
|
) {
|
|
46
|
-
issues.push(issue("incomplete-retention-schedule-document", "Complete the Data Retention Schedule document, its owner, linked Control, and Markdown before requesting approval of the complete schedule."));
|
|
46
|
+
issues.push(issue("incomplete-retention-schedule-document", "Complete the Data Retention Schedule document, its owner, linked Control, and Markdown before requesting approval of the complete schedule.", schedule.id));
|
|
47
47
|
}
|
|
48
48
|
if (
|
|
49
49
|
schedule
|
|
@@ -60,7 +60,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
60
60
|
? activeRows.filter(({ scheduleDocumentId }) => scheduleDocumentId !== schedule.id)
|
|
61
61
|
: [];
|
|
62
62
|
if (mismatched.length) {
|
|
63
|
-
issues.push(issue("retention-row-wrong-schedule-document", `Link every active retention row to the current Data Retention Schedule document before approval: ${mismatched[0].title}
|
|
63
|
+
issues.push(issue("retention-row-wrong-schedule-document", `Link every active retention row to the current Data Retention Schedule document before approval: ${mismatched[0].title}.`, mismatched[0].id));
|
|
64
64
|
}
|
|
65
65
|
const revisions = resourceReviewRevisionsSync(
|
|
66
66
|
loaded,
|
|
@@ -68,7 +68,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
68
68
|
);
|
|
69
69
|
const incomplete = activeRows.filter((row) => !retentionRuleIsCurrent(row, revisions, byId, loaded));
|
|
70
70
|
if (incomplete.length) {
|
|
71
|
-
issues.push(issue("incomplete-retention-schedule-row", `Complete the retention decisions before approving the schedule revision: ${incomplete[0].title}
|
|
71
|
+
issues.push(issue("incomplete-retention-schedule-row", `Complete the retention decisions before approving the schedule revision: ${incomplete[0].title}.`, incomplete[0].id));
|
|
72
72
|
}
|
|
73
73
|
const proposedRows = rows.filter(rowProposesCoverage);
|
|
74
74
|
const scopedPrograms = program.programIds
|
|
@@ -96,8 +96,8 @@ function sameIds(left = [], right = []) {
|
|
|
96
96
|
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function issue(code, message) {
|
|
100
|
-
return { code, message };
|
|
99
|
+
function issue(code, message, resourceId) {
|
|
100
|
+
return { code, message, ...(resourceId ? { resourceId } : {}) };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function primaryMarkdown(loaded, record) {
|
package/src/server.js
CHANGED
|
@@ -670,8 +670,9 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
670
670
|
: await createResourceDetail(input, type, id, { includeHistory: url.searchParams.get("history") !== "false" });
|
|
671
671
|
if (!entry) return json(response, 404, { error: "Resource not found." });
|
|
672
672
|
if (includeWorkflow && !session) {
|
|
673
|
-
const
|
|
674
|
-
|
|
673
|
+
const loaded = await loadWorkspace(input);
|
|
674
|
+
const workflow = await assessWorkflow(loaded, { programId: requestOptions.programId });
|
|
675
|
+
entry.workflow = workflowForResource(workflow, type, id, loaded);
|
|
675
676
|
}
|
|
676
677
|
return json(response, 200, entry);
|
|
677
678
|
}
|
|
@@ -1008,7 +1009,7 @@ async function loadStateSessionResource(session, token, type, id, serverOptions,
|
|
|
1008
1009
|
historyDeadlineAt: deadlineAt,
|
|
1009
1010
|
strictHistory: repository?.git?.available === true
|
|
1010
1011
|
}));
|
|
1011
|
-
detail.workflow = workflowForResource(workflow, type, id);
|
|
1012
|
+
detail.workflow = workflowForResource(workflow, type, id, session.loaded);
|
|
1012
1013
|
}
|
|
1013
1014
|
await verifyStateSessionSnapshot(session, performance.now(), deadlineAt);
|
|
1014
1015
|
assertCurrentStateSession(session);
|
package/src/web.js
CHANGED
|
@@ -578,8 +578,8 @@ function readinessOverview() {
|
|
|
578
578
|
const stage = READINESS_STAGES.find((candidate) => candidate.id === id);
|
|
579
579
|
const current = stageProgress(stage);
|
|
580
580
|
const remaining = current.total - current.complete;
|
|
581
|
-
const status =
|
|
582
|
-
return [stage.title, stage.summary, href, status,
|
|
581
|
+
const status = current.status === "In progress" ? remaining + " pages need work" : current.status;
|
|
582
|
+
return [stage.title, stage.summary, href, status, current.tone];
|
|
583
583
|
};
|
|
584
584
|
const stages = [
|
|
585
585
|
programStage("scope", "#/stage/scope"),
|
|
@@ -787,6 +787,7 @@ function wireRetentionScheduleTable(total, initialPage = 1) {
|
|
|
787
787
|
function renderRetentionScheduleIssues() {
|
|
788
788
|
const actions = retentionSchedulePageItems().filter((item) => (
|
|
789
789
|
item.id !== "collection-review-retention-schedule-item"
|
|
790
|
+
|| !item.commands?.some((command) => command.includes(" review-collection retention-schedule-item"))
|
|
790
791
|
));
|
|
791
792
|
if (!actions.length) return "";
|
|
792
793
|
return '<section class="retention-issues"><div class="section-head"><div><p class="kicker">Before approval</p><h2>Items to resolve</h2></div></div><div class="retention-issue-list">' + actions.map((item) => {
|
|
@@ -1719,7 +1720,7 @@ function retentionSchedulePageItems() {
|
|
|
1719
1720
|
key: item.id,
|
|
1720
1721
|
state: "ready",
|
|
1721
1722
|
href: item.id === "collection-review-retention-schedule-item"
|
|
1722
|
-
?
|
|
1723
|
+
? retentionCollectionActionHref(item)
|
|
1723
1724
|
: item.id?.startsWith("retention-use-")
|
|
1724
1725
|
? retentionScheduleItemHref(item)
|
|
1725
1726
|
: item.resourceId && item.resourceType
|
|
@@ -1732,6 +1733,19 @@ function retentionSchedulePageItems() {
|
|
|
1732
1733
|
]);
|
|
1733
1734
|
}
|
|
1734
1735
|
|
|
1736
|
+
function retentionCollectionActionHref(item) {
|
|
1737
|
+
if (item.resourceType === "document") return item.resourceId
|
|
1738
|
+
? '#/resource/document/' + encodeURIComponent(item.resourceId) + '?stage=policies'
|
|
1739
|
+
: item.createResource
|
|
1740
|
+
? '#/resources/document?new=1&documentScope=program&documentKind=schedule&programRole=required&title=Data%20Retention%20Schedule'
|
|
1741
|
+
: '#/resources/document';
|
|
1742
|
+
if (item.resourceType === "information-type") return '#/resources/information-type?review-collection=1';
|
|
1743
|
+
if (item.resourceType === "retention-schedule-item" && item.resourceId && item.resourceId !== item.id) {
|
|
1744
|
+
return '#/resource/retention-schedule-item/' + encodeURIComponent(item.resourceId) + '?stage=policies';
|
|
1745
|
+
}
|
|
1746
|
+
return "#/retention-schedule";
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1735
1749
|
function distinctPageActionItems(items) {
|
|
1736
1750
|
const distinct = new Map();
|
|
1737
1751
|
for (const item of items) {
|
|
@@ -1842,14 +1856,14 @@ function derivedStagePageState(stage, destination) {
|
|
|
1842
1856
|
.filter(({ id }) => id.startsWith("source-family-")) || [];
|
|
1843
1857
|
const incomplete = evidenceItems.filter(({ status }) => status !== "complete").length;
|
|
1844
1858
|
return incomplete
|
|
1845
|
-
? { complete: false, label: incomplete + " " + pluralize("source", incomplete) + " need work" }
|
|
1859
|
+
? { complete: false, label: incomplete + " " + pluralize("source", incomplete) + (incomplete === 1 ? " needs work" : " need work") }
|
|
1846
1860
|
: { complete: evidenceItems.length > 0, label: evidenceItems.length ? "Ready" : "Not configured", countsTowardProgress: evidenceItems.length > 0 };
|
|
1847
1861
|
}
|
|
1848
1862
|
if (destination.utility === "retention-schedule") {
|
|
1849
1863
|
const incomplete = retentionSchedulePageItems().length;
|
|
1850
1864
|
return !incomplete
|
|
1851
1865
|
? { complete: true, label: "Approved" }
|
|
1852
|
-
: { complete: false, label: incomplete ? incomplete + " " + pluralize("item", incomplete) + " need work" : "Approval needed" };
|
|
1866
|
+
: { complete: false, label: incomplete ? incomplete + " " + pluralize("item", incomplete) + (incomplete === 1 ? " needs work" : " need work") : "Approval needed" };
|
|
1853
1867
|
}
|
|
1854
1868
|
const items = stagePageItems(stage, destination);
|
|
1855
1869
|
const deferredStates = new Set(["later", "scheduled", "upcoming", "waiting-external"]);
|
|
@@ -3223,6 +3237,7 @@ function renderList(main, type, params = new URLSearchParams()) {
|
|
|
3223
3237
|
...(params.get("title") ? { title: params.get("title") } : {}),
|
|
3224
3238
|
...(params.get("documentKind") ? { documentKind: params.get("documentKind") } : {}),
|
|
3225
3239
|
...(documentScope ? { workflowScope: documentScope } : {}),
|
|
3240
|
+
...(params.get("programRole") ? { programRole: params.get("programRole") } : {}),
|
|
3226
3241
|
...relationshipSeed
|
|
3227
3242
|
} : {
|
|
3228
3243
|
...(params.get("title") ? { title: params.get("title") } : {}),
|
package/src/workflow.js
CHANGED
|
@@ -2,6 +2,7 @@ import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { modelSupports } from "../model/index.js";
|
|
5
|
+
import { buildActionContext } from "./action-context.js";
|
|
5
6
|
import { applicabilityReviewIsCurrent, applicabilityScopeRevision } from "./applicability-scope.js";
|
|
6
7
|
import { retentionScheduleIsAuthoritative } from "./collection-review.js";
|
|
7
8
|
import { assessRequiredAppointments } from "./appointments.js";
|
|
@@ -194,7 +195,7 @@ async function assessWorkflowUnmeasured(input, options = {}) {
|
|
|
194
195
|
findings: findings.sort(compareFindings),
|
|
195
196
|
workItems: workItems.sort(compareWorkItems),
|
|
196
197
|
recommended: recommended
|
|
197
|
-
? { ...recommended, rankingReason: rankingReason(recommended) }
|
|
198
|
+
? { ...recommended, rankingReason: rankingReason(recommended), context: buildActionContext(loaded, recommended) }
|
|
198
199
|
: null,
|
|
199
200
|
counts: {
|
|
200
201
|
findings: countBy(findings, "state"),
|
|
@@ -219,7 +220,7 @@ export function buildWorkflowDelta(before, after) {
|
|
|
219
220
|
};
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
export function workflowForResource(workflow, type, id) {
|
|
223
|
+
export function workflowForResource(workflow, type, id, loaded = null) {
|
|
223
224
|
if (!workflow) return {
|
|
224
225
|
findings: [],
|
|
225
226
|
workItems: [],
|
|
@@ -246,12 +247,16 @@ export function workflowForResource(workflow, type, id) {
|
|
|
246
247
|
related: {
|
|
247
248
|
findings: relatedFindings,
|
|
248
249
|
workItems: relatedWorkItems,
|
|
249
|
-
recommended: [...relatedFindings, ...relatedWorkItems].sort(compareRecommended)[0]
|
|
250
|
+
recommended: contextualAction([...relatedFindings, ...relatedWorkItems].sort(compareRecommended)[0], loaded)
|
|
250
251
|
},
|
|
251
|
-
recommended: [...findings, ...workItems].sort(compareRecommended)[0]
|
|
252
|
+
recommended: contextualAction([...findings, ...workItems].sort(compareRecommended)[0], loaded)
|
|
252
253
|
};
|
|
253
254
|
}
|
|
254
255
|
|
|
256
|
+
function contextualAction(item, loaded) {
|
|
257
|
+
return item && loaded ? { ...item, context: buildActionContext(loaded, item) } : item || null;
|
|
258
|
+
}
|
|
259
|
+
|
|
255
260
|
export async function previewWorkflowMutation(input, mutation) {
|
|
256
261
|
const loaded = await loadWorkspace(input);
|
|
257
262
|
const previewRoot = await mkdtemp(join(tmpdir(), "filegrc-workflow-preview-"));
|
|
@@ -1129,6 +1134,9 @@ function normalizeFinding(code, item, context) {
|
|
|
1129
1134
|
title: item.title,
|
|
1130
1135
|
message: item.message,
|
|
1131
1136
|
...(item.nextSteps?.length ? { nextSteps: item.nextSteps } : {}),
|
|
1137
|
+
...(item.checks ? { unmetChecks: Object.entries(item.checks)
|
|
1138
|
+
.filter(([, passed]) => passed === false)
|
|
1139
|
+
.map(([name]) => name) } : {}),
|
|
1132
1140
|
...(item.resourceId ? { resourceId: item.resourceId } : {}),
|
|
1133
1141
|
...(item.createResourceType ? { createResourceType: item.createResourceType } : {}),
|
|
1134
1142
|
...(item.sourceResourceIds ? { sourceResourceIds: item.sourceResourceIds } : {}),
|