@greenarmor/ges-web-dashboard 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +129 -1
- package/dist/template.js +314 -13
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
|
-
import type { ScoreFile, Control, FixHistoryEntry, ActivityLogEntry, GovernanceRecord, GovernanceVerificationResult } from "@greenarmor/ges-core";
|
|
2
|
+
import type { ScoreFile, Control, FixHistoryEntry, ActivityLogEntry, GovernanceRecord, GovernanceVerificationResult, FixAssignment } from "@greenarmor/ges-core";
|
|
3
3
|
import type { Finding } from "@greenarmor/ges-audit-engine";
|
|
4
4
|
export interface DashboardOptions {
|
|
5
5
|
port?: number;
|
|
@@ -79,6 +79,7 @@ export interface DashboardData {
|
|
|
79
79
|
fixHistory: FixHistoryEntry[];
|
|
80
80
|
activityLog: ActivityLogEntry[];
|
|
81
81
|
governance: GovernanceData;
|
|
82
|
+
fixAssignments: FixAssignment[];
|
|
82
83
|
lastAudit: string;
|
|
83
84
|
}
|
|
84
85
|
export interface GovernanceData {
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getAllPacks, getPack } from "@greenarmor/ges-policy-engine";
|
|
|
6
6
|
import { generateScoreFile } from "@greenarmor/ges-scoring-engine";
|
|
7
7
|
import { loadFixHistory, loadActivityLog, loadControlsFromDisk, loadControlOverrides, applyOverridesToControls } from "@greenarmor/ges-core";
|
|
8
8
|
import { loadGovernanceRecords, verifyGovernanceRecord, verifyAllGovernanceRecords, createGovernanceRecord, addGovernanceRecord, findGovernanceRecord, setGovernanceApproval, addGovernanceEvidence, createEvidenceRef, deleteGovernanceRecord, setGovernanceRiskAssessment, setGovernancePolicyBasis, setGovernanceReviewCycle, setGovernanceDataInventory, setGovernanceComplianceLinks, setGovernanceCommittee, recordActivity, } from "@greenarmor/ges-core";
|
|
9
|
+
import { loadFixAssignments, createFixAssignment, addFixAssignment, resolveFixAssignment, unassignFix, } from "@greenarmor/ges-core";
|
|
9
10
|
import { getInstalledPackIds as getInstalledPackIdsFromDisk } from "@greenarmor/ges-core";
|
|
10
11
|
import { generateMarkdownReport, generateHtmlReport } from "@greenarmor/ges-report-generator";
|
|
11
12
|
import { renderDashboard } from "./template.js";
|
|
@@ -179,6 +180,7 @@ export function collectDashboardData(projectPath) {
|
|
|
179
180
|
const fixHistory = loadFixHistory(projectPath);
|
|
180
181
|
const activityLog = loadActivityLog(projectPath);
|
|
181
182
|
const governance = collectGovernanceData(projectPath);
|
|
183
|
+
const fixAssignments = loadFixAssignments(projectPath);
|
|
182
184
|
const metadataPath = path.join(projectPath, ".ges", "metadata.json");
|
|
183
185
|
let lastAudit = "";
|
|
184
186
|
try {
|
|
@@ -193,7 +195,7 @@ export function collectDashboardData(projectPath) {
|
|
|
193
195
|
projectName: config?.project_name || "Unknown Project",
|
|
194
196
|
projectType: config?.project_type || "unknown",
|
|
195
197
|
frameworks: allFrameworks,
|
|
196
|
-
gesfVersion: "1.
|
|
198
|
+
gesfVersion: "1.5.1",
|
|
197
199
|
score,
|
|
198
200
|
controls,
|
|
199
201
|
findings,
|
|
@@ -201,6 +203,7 @@ export function collectDashboardData(projectPath) {
|
|
|
201
203
|
fixHistory,
|
|
202
204
|
activityLog,
|
|
203
205
|
governance,
|
|
206
|
+
fixAssignments,
|
|
204
207
|
lastAudit,
|
|
205
208
|
};
|
|
206
209
|
}
|
|
@@ -543,6 +546,121 @@ export function startDashboard(options) {
|
|
|
543
546
|
}
|
|
544
547
|
return;
|
|
545
548
|
}
|
|
549
|
+
if (req.method === "POST" && pathname === "/api/fix-assignments/assign") {
|
|
550
|
+
try {
|
|
551
|
+
const body = await readBody(req);
|
|
552
|
+
const fk = String(body.finding_key || "").trim();
|
|
553
|
+
const recordId = String(body.governance_record_id || "").trim();
|
|
554
|
+
const assignee = String(body.assignee || "").trim();
|
|
555
|
+
if (!fk || !recordId || !assignee) {
|
|
556
|
+
jsonError(res, "finding_key, governance_record_id, and assignee are required", 400);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const record = findGovernanceRecord(options.projectPath, recordId);
|
|
560
|
+
if (!record) {
|
|
561
|
+
jsonError(res, `Governance record not found: ${recordId}`, 404);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const assignment = createFixAssignment({
|
|
565
|
+
finding_key: fk,
|
|
566
|
+
finding_rule_id: String(body.finding_rule_id || ""),
|
|
567
|
+
finding_title: String(body.finding_title || ""),
|
|
568
|
+
finding_file: String(body.finding_file || ""),
|
|
569
|
+
finding_line: body.finding_line ? Number(body.finding_line) : undefined,
|
|
570
|
+
finding_severity: body.finding_severity || "medium",
|
|
571
|
+
finding_control_ids: parseList(body.finding_control_ids),
|
|
572
|
+
governance_record_id: record.id,
|
|
573
|
+
governance_system_name: record.system_name,
|
|
574
|
+
assignee,
|
|
575
|
+
assignee_role: String(body.assignee_role || ""),
|
|
576
|
+
assigned_by: String(body.assigned_by || body.actor_name || "dashboard"),
|
|
577
|
+
notes: String(body.notes || ""),
|
|
578
|
+
});
|
|
579
|
+
addFixAssignment(options.projectPath, assignment);
|
|
580
|
+
recordActivity(options.projectPath, {
|
|
581
|
+
source: "cli",
|
|
582
|
+
action: "fix_assign",
|
|
583
|
+
title: `Fix assigned: ${assignment.finding_rule_id} → ${record.system_name}`,
|
|
584
|
+
description: `Assigned ${assignment.finding_rule_id} (${assignment.finding_title}) to ${assignee} (${assignment.assignee_role || "unspecified role"}), linked to governance record ${record.system_name}.`,
|
|
585
|
+
details: {
|
|
586
|
+
finding_key: fk,
|
|
587
|
+
governance_record_id: record.id,
|
|
588
|
+
assignee,
|
|
589
|
+
governance_system_name: record.system_name,
|
|
590
|
+
},
|
|
591
|
+
actor_name: body.actor_name ? String(body.actor_name) : undefined,
|
|
592
|
+
actor_role: body.actor_role ? String(body.actor_role) : undefined,
|
|
593
|
+
});
|
|
594
|
+
jsonResponse(res, { success: true, assignment });
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
jsonError(res, err instanceof Error ? err.message : String(err));
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (req.method === "POST" && pathname === "/api/fix-assignments/resolve") {
|
|
602
|
+
try {
|
|
603
|
+
const body = await readBody(req);
|
|
604
|
+
const fk = String(body.finding_key || "").trim();
|
|
605
|
+
if (!fk) {
|
|
606
|
+
jsonError(res, "finding_key is required", 400);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const resolved = resolveFixAssignment(options.projectPath, fk, {
|
|
610
|
+
resolved_by: String(body.resolved_by || body.actor_name || "dashboard"),
|
|
611
|
+
resolved_by_role: String(body.resolved_by_role || body.actor_role || ""),
|
|
612
|
+
method: body.method || "manual",
|
|
613
|
+
resolution_notes: String(body.resolution_notes || ""),
|
|
614
|
+
});
|
|
615
|
+
if (!resolved) {
|
|
616
|
+
jsonError(res, `Fix assignment not found for finding_key: ${fk}`, 404);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
recordActivity(options.projectPath, {
|
|
620
|
+
source: "cli",
|
|
621
|
+
action: "fix_resolve",
|
|
622
|
+
title: `Fix resolved: ${resolved.finding_rule_id}`,
|
|
623
|
+
description: `Resolved ${resolved.finding_rule_id} via ${body.method || "manual"} by ${body.resolved_by || body.actor_name || "dashboard"}.`,
|
|
624
|
+
details: {
|
|
625
|
+
finding_key: fk,
|
|
626
|
+
governance_record_id: resolved.governance_record_id,
|
|
627
|
+
method: body.method || "manual",
|
|
628
|
+
},
|
|
629
|
+
actor_name: body.actor_name ? String(body.actor_name) : undefined,
|
|
630
|
+
actor_role: body.actor_role ? String(body.actor_role) : undefined,
|
|
631
|
+
});
|
|
632
|
+
jsonResponse(res, { success: true, assignment: resolved });
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
jsonError(res, err instanceof Error ? err.message : String(err));
|
|
636
|
+
}
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const fixAssignDeleteMatch = req.method === "POST" ? pathname.match(/^\/api\/fix-assignments\/([^/]+)\/unassign$/) : null;
|
|
640
|
+
if (fixAssignDeleteMatch) {
|
|
641
|
+
try {
|
|
642
|
+
const fkey = decodeURIComponent(fixAssignDeleteMatch[1]);
|
|
643
|
+
const deleted = unassignFix(options.projectPath, fkey);
|
|
644
|
+
if (!deleted) {
|
|
645
|
+
jsonError(res, `Fix assignment not found for finding: ${fkey}`, 404);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
recordActivity(options.projectPath, {
|
|
649
|
+
source: "cli",
|
|
650
|
+
action: "fix_assign",
|
|
651
|
+
title: `Fix assignment removed`,
|
|
652
|
+
description: `Removed fix assignment for finding ${fkey}.`,
|
|
653
|
+
details: { finding_key: fkey },
|
|
654
|
+
actor_name: undefined,
|
|
655
|
+
actor_role: undefined,
|
|
656
|
+
});
|
|
657
|
+
jsonResponse(res, { success: true });
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
jsonError(res, err instanceof Error ? err.message : String(err));
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
546
664
|
if (req.method !== "GET") {
|
|
547
665
|
jsonError(res, "Method not allowed", 405);
|
|
548
666
|
return;
|
|
@@ -590,6 +708,16 @@ export function startDashboard(options) {
|
|
|
590
708
|
}
|
|
591
709
|
return;
|
|
592
710
|
}
|
|
711
|
+
if (pathname === "/api/fix-assignments") {
|
|
712
|
+
try {
|
|
713
|
+
const data = collectDashboardData(options.projectPath);
|
|
714
|
+
jsonResponse(res, data.fixAssignments);
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
jsonError(res, err instanceof Error ? err.message : String(err));
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
593
721
|
if (pathname === "/api/activity-log") {
|
|
594
722
|
try {
|
|
595
723
|
const data = collectDashboardData(options.projectPath);
|
package/dist/template.js
CHANGED
|
@@ -74,6 +74,10 @@ export function renderDashboard(data) {
|
|
|
74
74
|
const controls = data.controls;
|
|
75
75
|
const packs = data.packs;
|
|
76
76
|
const governance = data.governance;
|
|
77
|
+
const fixAssignmentMap = new Map();
|
|
78
|
+
for (const a of data.fixAssignments) {
|
|
79
|
+
fixAssignmentMap.set(a.finding_key, a);
|
|
80
|
+
}
|
|
77
81
|
const findingsBySeverity = {
|
|
78
82
|
critical: findings.filter(f => f.severity === "critical").length,
|
|
79
83
|
high: findings.filter(f => f.severity === "high").length,
|
|
@@ -574,7 +578,7 @@ export function renderDashboard(data) {
|
|
|
574
578
|
</div>
|
|
575
579
|
|
|
576
580
|
<div id="fixes-tab-pending" class="tab-panel">
|
|
577
|
-
${renderComplianceFixCards(complianceIssues, "fix")}
|
|
581
|
+
${renderComplianceFixCards(complianceIssues, "fix", fixAssignmentMap, governance.records)}
|
|
578
582
|
</div>
|
|
579
583
|
</div>
|
|
580
584
|
|
|
@@ -656,7 +660,7 @@ export function renderDashboard(data) {
|
|
|
656
660
|
</div>
|
|
657
661
|
|
|
658
662
|
<div id="trace-tab-fixes" class="tab-panel">
|
|
659
|
-
${renderComplianceFixCards(complianceIssues, "trace")}
|
|
663
|
+
${renderComplianceFixCards(complianceIssues, "trace", fixAssignmentMap, governance.records)}
|
|
660
664
|
</div>
|
|
661
665
|
|
|
662
666
|
<div id="trace-tab-controls" class="tab-panel">
|
|
@@ -695,10 +699,14 @@ export function renderDashboard(data) {
|
|
|
695
699
|
|
|
696
700
|
<div id="control-detail-modal" style="display:none;"></div>
|
|
697
701
|
|
|
702
|
+
<div class="gov-modal-overlay" id="assign-modal-overlay" onclick="if(event.target===this)closeAssignModal()">
|
|
703
|
+
<div class="gov-modal" id="assign-modal"></div>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
698
706
|
</div>
|
|
699
707
|
|
|
700
708
|
<div class="footer">
|
|
701
|
-
Generated by GESF v${escapeHtml(data.gesfVersion)} | Last audit: ${escapeHtml(new Date(data.lastAudit).toLocaleString())} | <a href="/api/data">JSON API</a> | <a href="/api/packs">Packs API</a> | <a href="/api/fix-history">Fix History API</a> | <a href="/api/activity-log">Activity Log API</a> | <a href="/api/governance">Governance API</a>
|
|
709
|
+
Generated by GESF v${escapeHtml(data.gesfVersion)} | Last audit: ${escapeHtml(new Date(data.lastAudit).toLocaleString())} | <a href="/api/data">JSON API</a> | <a href="/api/packs">Packs API</a> | <a href="/api/fix-history">Fix History API</a> | <a href="/api/fix-assignments">Fix Assignments API</a> | <a href="/api/activity-log">Activity Log API</a> | <a href="/api/governance">Governance API</a>
|
|
702
710
|
</div>
|
|
703
711
|
|
|
704
712
|
<script>
|
|
@@ -1149,6 +1157,130 @@ export function renderDashboard(data) {
|
|
|
1149
1157
|
c.appendChild(t);
|
|
1150
1158
|
setTimeout(function() { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(function() { if(t.parentNode) t.remove(); }, 300); }, 3000);
|
|
1151
1159
|
};
|
|
1160
|
+
|
|
1161
|
+
var govRecordsForAssign = ${JSON.stringify(governance.records.map(r => ({ id: r.id, name: r.system_name, status: r.status, risk: r.risk_level })))};
|
|
1162
|
+
|
|
1163
|
+
window.openAssignModal = function(fkey, ruleId, title, file, line, severity, controlIds) {
|
|
1164
|
+
var overlay = document.getElementById('assign-modal-overlay');
|
|
1165
|
+
var modal = document.getElementById('assign-modal');
|
|
1166
|
+
if (!overlay || !modal) return;
|
|
1167
|
+
if (govRecordsForAssign.length === 0) {
|
|
1168
|
+
showToast('No governance records found. Create one first on the Governance tab.', 'error');
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
var recordOptions = govRecordsForAssign.map(function(r) {
|
|
1172
|
+
return '<option value="' + r.id + '">' + r.name + ' (' + r.status + ', ' + r.risk + ' risk)</option>';
|
|
1173
|
+
}).join('');
|
|
1174
|
+
modal.innerHTML =
|
|
1175
|
+
'<div class="gov-modal-header"><h3>Assign Fix to Governance Record</h3><button class="gov-modal-close" onclick="closeAssignModal()">×</button></div>' +
|
|
1176
|
+
'<div class="gov-modal-body">' +
|
|
1177
|
+
'<div style="margin-bottom:12px;padding:8px 10px;background:#f9fafb;border-radius:6px;font-size:12px;">' +
|
|
1178
|
+
'<strong>' + ruleId + '</strong> — ' + title + '<br>' +
|
|
1179
|
+
'<span style="color:#6b7280;font-family:monospace;">' + file + (line ? ':' + line : '') + '</span>' +
|
|
1180
|
+
'</div>' +
|
|
1181
|
+
'<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Governance Record *</label>' +
|
|
1182
|
+
'<select name="governance_record_id" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;">' + recordOptions + '</select>' +
|
|
1183
|
+
'<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Assignee Name *</label>' +
|
|
1184
|
+
'<input name="assignee" type="text" placeholder="e.g., Jane Doe" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
|
|
1185
|
+
'<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Assignee Role</label>' +
|
|
1186
|
+
'<input name="assignee_role" type="text" placeholder="e.g., Security Engineer" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
|
|
1187
|
+
'<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Notes</label>' +
|
|
1188
|
+
'<textarea name="notes" rows="2" placeholder="Optional context for this assignment..." style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;"></textarea>' +
|
|
1189
|
+
'<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Your Name (for audit log)</label>' +
|
|
1190
|
+
'<input name="actor_name" type="text" placeholder="Who is making this assignment" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
|
|
1191
|
+
'<label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Your Role</label>' +
|
|
1192
|
+
'<input name="actor_role" type="text" placeholder="e.g., Tech Lead" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;margin-bottom:12px;" />' +
|
|
1193
|
+
'</div>' +
|
|
1194
|
+
'<div class="gov-modal-footer"><button class="gov-btn gov-btn-outline" onclick="closeAssignModal()">Cancel</button><button class="gov-btn gov-btn-primary" onclick="submitAssignForm()">Assign</button></div>';
|
|
1195
|
+
modal.dataset.fkey = fkey;
|
|
1196
|
+
modal.dataset.ruleId = ruleId;
|
|
1197
|
+
modal.dataset.title = title;
|
|
1198
|
+
modal.dataset.file = file;
|
|
1199
|
+
modal.dataset.line = String(line || 0);
|
|
1200
|
+
modal.dataset.severity = severity;
|
|
1201
|
+
modal.dataset.controlIds = controlIds;
|
|
1202
|
+
overlay.classList.add('active');
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
window.closeAssignModal = function() {
|
|
1206
|
+
var overlay = document.getElementById('assign-modal-overlay');
|
|
1207
|
+
var modal = document.getElementById('assign-modal');
|
|
1208
|
+
if (overlay) overlay.classList.remove('active');
|
|
1209
|
+
if (modal) modal.innerHTML = '';
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
window.submitAssignForm = function() {
|
|
1213
|
+
var modal = document.getElementById('assign-modal');
|
|
1214
|
+
if (!modal) return;
|
|
1215
|
+
var inputs = modal.querySelectorAll('[name]');
|
|
1216
|
+
var body = {
|
|
1217
|
+
finding_key: modal.dataset.fkey,
|
|
1218
|
+
finding_rule_id: modal.dataset.ruleId,
|
|
1219
|
+
finding_title: modal.dataset.title,
|
|
1220
|
+
finding_file: modal.dataset.file,
|
|
1221
|
+
finding_line: parseInt(modal.dataset.line, 10),
|
|
1222
|
+
finding_severity: modal.dataset.severity,
|
|
1223
|
+
finding_control_ids: (modal.dataset.controlIds || '').split(',').filter(Boolean),
|
|
1224
|
+
};
|
|
1225
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
1226
|
+
body[inputs[i].name] = inputs[i].value;
|
|
1227
|
+
}
|
|
1228
|
+
var btn = modal.querySelector('.gov-btn-primary');
|
|
1229
|
+
if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
|
|
1230
|
+
fetch('/api/fix-assignments/assign', {
|
|
1231
|
+
method: 'POST',
|
|
1232
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1233
|
+
body: JSON.stringify(body),
|
|
1234
|
+
})
|
|
1235
|
+
.then(function(r) { return r.json(); })
|
|
1236
|
+
.then(function(d) {
|
|
1237
|
+
if (d.success) { closeAssignModal(); showToast('Fix assigned! Reloading...', 'success'); setTimeout(function() { location.reload(); }, 800); }
|
|
1238
|
+
else { showToast(d.error || 'Failed', 'error'); if (btn) { btn.textContent = 'Assign'; btn.disabled = false; } }
|
|
1239
|
+
})
|
|
1240
|
+
.catch(function(e) { showToast('Error: ' + (e.message||'network'), 'error'); if (btn) { btn.textContent = 'Assign'; btn.disabled = false; } });
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
window.resolveFindingFix = function(fkey) {
|
|
1244
|
+
if (!confirm('Mark this fix as resolved?')) return;
|
|
1245
|
+
var resolver = prompt('Your name:', '') || 'dashboard';
|
|
1246
|
+
var resolverRole = prompt('Your role:', '') || '';
|
|
1247
|
+
var method = prompt('Method (auto-fix / manual / not-applicable):', 'manual') || 'manual';
|
|
1248
|
+
var notes = prompt('Resolution notes (optional):', '') || '';
|
|
1249
|
+
fetch('/api/fix-assignments/resolve', {
|
|
1250
|
+
method: 'POST',
|
|
1251
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1252
|
+
body: JSON.stringify({
|
|
1253
|
+
finding_key: fkey,
|
|
1254
|
+
resolved_by: resolver,
|
|
1255
|
+
resolved_by_role: resolverRole,
|
|
1256
|
+
method: method,
|
|
1257
|
+
resolution_notes: notes,
|
|
1258
|
+
actor_name: resolver,
|
|
1259
|
+
actor_role: resolverRole,
|
|
1260
|
+
}),
|
|
1261
|
+
})
|
|
1262
|
+
.then(function(r) { return r.json(); })
|
|
1263
|
+
.then(function(d) {
|
|
1264
|
+
if (d.success) { showToast('Fix resolved! Reloading...', 'success'); setTimeout(function() { location.reload(); }, 800); }
|
|
1265
|
+
else { showToast(d.error || 'Failed', 'error'); }
|
|
1266
|
+
})
|
|
1267
|
+
.catch(function(e) { showToast('Error: ' + (e.message||'network'), 'error'); });
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
window.unassignFix = function(fkey) {
|
|
1271
|
+
if (!confirm('Remove this fix assignment?')) return;
|
|
1272
|
+
fetch('/api/fix-assignments/' + encodeURIComponent(fkey) + '/unassign', {
|
|
1273
|
+
method: 'POST',
|
|
1274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1275
|
+
body: '{}',
|
|
1276
|
+
})
|
|
1277
|
+
.then(function(r) { return r.json(); })
|
|
1278
|
+
.then(function(d) {
|
|
1279
|
+
if (d.success) { showToast('Unassigned! Reloading...', 'success'); setTimeout(function() { location.reload(); }, 800); }
|
|
1280
|
+
else { showToast(d.error || 'Failed', 'error'); }
|
|
1281
|
+
})
|
|
1282
|
+
.catch(function(e) { showToast('Error: ' + (e.message||'network'), 'error'); });
|
|
1283
|
+
};
|
|
1152
1284
|
})();
|
|
1153
1285
|
</script>
|
|
1154
1286
|
|
|
@@ -1531,13 +1663,14 @@ function renderComplianceIssuesTable(issues) {
|
|
|
1531
1663
|
</tbody>
|
|
1532
1664
|
</table>`;
|
|
1533
1665
|
}
|
|
1534
|
-
function renderComplianceFixCards(issues, idPrefix) {
|
|
1666
|
+
function renderComplianceFixCards(issues, idPrefix, assignmentMap, govRecords) {
|
|
1535
1667
|
if (issues.length === 0) {
|
|
1536
1668
|
return '<div class="card"><div class="empty-state"><div class="icon">✓</div><div class="msg" style="color:#22c55e;">All controls passing</div><div class="sub">No fixes needed</div></div></div>';
|
|
1537
1669
|
}
|
|
1538
1670
|
const totalAuditFindings = issues.reduce((sum, i) => sum + i.auditFindings.length, 0);
|
|
1539
1671
|
const criticalCount = issues.filter(i => i.severity === "critical").length;
|
|
1540
1672
|
const highCount = issues.filter(i => i.severity === "high").length;
|
|
1673
|
+
const assignedCount = assignmentMap ? assignmentMap.size : 0;
|
|
1541
1674
|
let html = '';
|
|
1542
1675
|
html += `<div style="margin-bottom:20px;">`;
|
|
1543
1676
|
html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
|
|
@@ -1545,6 +1678,7 @@ function renderComplianceFixCards(issues, idPrefix) {
|
|
|
1545
1678
|
html += `<div class="card stat"><div class="num" style="color:#ef4444;">${criticalCount}</div><div class="label">Critical</div></div>`;
|
|
1546
1679
|
html += `<div class="card stat"><div class="num" style="color:#f97316;">${highCount}</div><div class="label">High</div></div>`;
|
|
1547
1680
|
html += `<div class="card stat"><div class="num">${totalAuditFindings}</div><div class="label">Audit Findings</div></div>`;
|
|
1681
|
+
html += `<div class="card stat"><div class="num" style="color:${assignedCount > 0 ? "#3b82f6" : "#9ca3af"};">${assignedCount}</div><div class="label">Assigned</div></div>`;
|
|
1548
1682
|
html += `</div>`;
|
|
1549
1683
|
html += `</div>`;
|
|
1550
1684
|
for (let i = 0; i < issues.length; i++) {
|
|
@@ -1571,8 +1705,10 @@ function renderComplianceFixCards(issues, idPrefix) {
|
|
|
1571
1705
|
html += `<div style="font-size:13px;color:#4b5563;line-height:1.6;">${escapeHtml(issue.description)}</div>`;
|
|
1572
1706
|
html += `</div>`;
|
|
1573
1707
|
if (issue.auditFindings.length > 0) {
|
|
1574
|
-
html += `<div class="fix-section"><div class="fix-section-title">Audit Evidence (${issue.auditFindings.length})</div>`;
|
|
1708
|
+
html += `<div class="fix-section"><div class="fix-section-title">Audit Evidence & Fix Assignments (${issue.auditFindings.length})</div>`;
|
|
1575
1709
|
for (const f of issue.auditFindings) {
|
|
1710
|
+
const fkey = `${f.ruleId}:${f.file}:${f.line || 0}`;
|
|
1711
|
+
const assignment = assignmentMap?.get(fkey);
|
|
1576
1712
|
html += `<div class="fix-finding-item ${f.severity}">`;
|
|
1577
1713
|
html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">`;
|
|
1578
1714
|
html += `<span class="badge badge-sev" style="background:${severityColor(f.severity)};font-size:10px;">${f.severity.toUpperCase()}</span>`;
|
|
@@ -1582,6 +1718,30 @@ function renderComplianceFixCards(issues, idPrefix) {
|
|
|
1582
1718
|
html += `<div style="font-size:12px;color:#4b5563;margin-top:4px;">${escapeHtml(f.description)}</div>`;
|
|
1583
1719
|
if (f.evidence)
|
|
1584
1720
|
html += `<div class="fix-evidence">${escapeHtml(f.evidence)}</div>`;
|
|
1721
|
+
if (assignment) {
|
|
1722
|
+
const statusColorAssign = assignment.status === "fixed" || assignment.status === "verified" ? "#22c55e" : assignment.status === "in-progress" ? "#3b82f6" : "#eab308";
|
|
1723
|
+
html += `<div class="fix-assign-box" style="margin-top:8px;padding:10px 12px;border-radius:8px;background:#f0fdf4;border:1px solid #bbf7d0;">`;
|
|
1724
|
+
html += `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">`;
|
|
1725
|
+
html += `<span class="badge" style="background:${statusColorAssign};color:#fff;font-size:10px;text-transform:uppercase;">${escapeHtml(assignment.status)}</span>`;
|
|
1726
|
+
html += `<span style="font-size:12px;font-weight:600;color:#166534;">Linked: ${escapeHtml(assignment.governance_system_name)}</span>`;
|
|
1727
|
+
html += `<span style="font-size:11px;color:#4b5563;">Assignee: ${escapeHtml(assignment.assignee)}${assignment.assignee_role ? ' (' + escapeHtml(assignment.assignee_role) + ')' : ''}</span>`;
|
|
1728
|
+
html += `</div>`;
|
|
1729
|
+
if (assignment.resolution) {
|
|
1730
|
+
html += `<div style="font-size:11px;color:#4b5563;margin-top:4px;">Resolved by ${escapeHtml(assignment.resolution.resolved_by)} via ${escapeHtml(assignment.resolution.method)} on ${escapeHtml(new Date(assignment.resolution.resolved_at).toLocaleDateString())}</div>`;
|
|
1731
|
+
}
|
|
1732
|
+
html += `<div style="margin-top:6px;display:flex;gap:6px;">`;
|
|
1733
|
+
if (assignment.status !== "fixed" && assignment.status !== "verified") {
|
|
1734
|
+
html += `<button class="gov-action-btn" style="background:#22c55e;color:#fff;border:none;padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();resolveFindingFix('${escapeHtml(fkey)}')">Mark Fixed</button>`;
|
|
1735
|
+
}
|
|
1736
|
+
html += `<button class="gov-action-btn" style="background:#fee2e2;color:#991b1b;border:none;padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();unassignFix('${escapeHtml(fkey)}')">Unassign</button>`;
|
|
1737
|
+
html += `</div>`;
|
|
1738
|
+
html += `</div>`;
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
html += `<div style="margin-top:8px;">`;
|
|
1742
|
+
html += `<button class="gov-action-btn" style="background:#3b82f6;color:#fff;border:none;padding:4px 12px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();openAssignModal('${escapeHtml(fkey)}','${escapeHtml(f.ruleId)}','${escapeHtml(f.title.replace(/'/g, "\\'"))}','${escapeHtml(f.file)}',${f.line || 0},'${escapeHtml(f.severity)}','${escapeHtml(f.controlIds.join(","))}')">+ Assign to Governance Record</button>`;
|
|
1743
|
+
html += `</div>`;
|
|
1744
|
+
}
|
|
1585
1745
|
html += `</div>`;
|
|
1586
1746
|
}
|
|
1587
1747
|
html += `</div>`;
|
|
@@ -1594,6 +1754,9 @@ function renderComplianceFixCards(issues, idPrefix) {
|
|
|
1594
1754
|
}
|
|
1595
1755
|
}
|
|
1596
1756
|
html += `</div>`;
|
|
1757
|
+
const ctrlFkey = `${issue.controlId}::0`;
|
|
1758
|
+
const ctrlAssignment = assignmentMap?.get(ctrlFkey);
|
|
1759
|
+
html += renderGovernanceProvenanceSection(issue.controlId, issue.controlName, issue.severity, ctrlFkey, ctrlAssignment, govRecords);
|
|
1597
1760
|
html += `<div class="fix-section"><div class="fix-section-title">Traceability</div>`;
|
|
1598
1761
|
html += `<table><tbody>`;
|
|
1599
1762
|
html += `<tr><td style="font-weight:600;width:160px;">Control</td><td><span class="link" onclick="showControlDetail('${escapeHtml(issue.controlId)}')">${escapeHtml(issue.controlId)}</span> — ${escapeHtml(issue.controlName)}</td></tr>`;
|
|
@@ -1610,6 +1773,142 @@ function renderComplianceFixCards(issues, idPrefix) {
|
|
|
1610
1773
|
}
|
|
1611
1774
|
return html;
|
|
1612
1775
|
}
|
|
1776
|
+
function renderGovernanceProvenanceSection(controlId, controlName, severity, ctrlFkey, ctrlAssignment, govRecords) {
|
|
1777
|
+
let html = `<div class="fix-section"><div class="fix-section-title">Governance Provenance Chain</div>`;
|
|
1778
|
+
if (ctrlAssignment && govRecords) {
|
|
1779
|
+
const record = govRecords.find(r => r.id === ctrlAssignment.governance_record_id);
|
|
1780
|
+
const aStatusColor = ctrlAssignment.status === "fixed" || ctrlAssignment.status === "verified"
|
|
1781
|
+
? "#22c55e"
|
|
1782
|
+
: ctrlAssignment.status === "in-progress"
|
|
1783
|
+
? "#3b82f6"
|
|
1784
|
+
: "#eab308";
|
|
1785
|
+
html += `<div style="padding:12px 16px;border-radius:8px;background:#f0fdf4;border:1px solid #bbf7d0;margin-bottom:10px;">`;
|
|
1786
|
+
html += `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px;">`;
|
|
1787
|
+
html += `<span class="badge" style="background:${aStatusColor};color:#fff;font-size:10px;text-transform:uppercase;">${escapeHtml(ctrlAssignment.status)}</span>`;
|
|
1788
|
+
html += `<span style="font-size:12px;font-weight:600;color:#166534;">Assignee: ${escapeHtml(ctrlAssignment.assignee)}${ctrlAssignment.assignee_role ? ' (' + escapeHtml(ctrlAssignment.assignee_role) + ')' : ''}</span>`;
|
|
1789
|
+
html += `<span style="font-size:11px;color:#6b7280;">Assigned by ${escapeHtml(ctrlAssignment.assigned_by)} on ${escapeHtml(new Date(ctrlAssignment.assigned_at).toLocaleDateString())}</span>`;
|
|
1790
|
+
html += `</div>`;
|
|
1791
|
+
if (ctrlAssignment.notes) {
|
|
1792
|
+
html += `<div style="font-size:12px;color:#4b5563;margin-bottom:6px;"><strong>Notes:</strong> ${escapeHtml(ctrlAssignment.notes)}</div>`;
|
|
1793
|
+
}
|
|
1794
|
+
if (ctrlAssignment.resolution) {
|
|
1795
|
+
const r = ctrlAssignment.resolution;
|
|
1796
|
+
html += `<div style="font-size:12px;padding:6px 10px;background:#dcfce7;border-radius:6px;margin-top:6px;">`;
|
|
1797
|
+
html += `<strong>✓ Resolved</strong> by ${escapeHtml(r.resolved_by)}${r.resolved_by_role ? ' (' + escapeHtml(r.resolved_by_role) + ')' : ''} via <strong>${escapeHtml(r.method)}</strong> on ${escapeHtml(new Date(r.resolved_at).toLocaleDateString())}`;
|
|
1798
|
+
if (r.resolution_notes)
|
|
1799
|
+
html += `<br><span style="color:#4b5563;">${escapeHtml(r.resolution_notes)}</span>`;
|
|
1800
|
+
html += `</div>`;
|
|
1801
|
+
}
|
|
1802
|
+
html += `<div style="margin-top:8px;display:flex;gap:6px;">`;
|
|
1803
|
+
if (ctrlAssignment.status !== "fixed" && ctrlAssignment.status !== "verified") {
|
|
1804
|
+
html += `<button class="gov-action-btn" style="background:#22c55e;color:#fff;border:none;padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();resolveFindingFix('${escapeHtml(ctrlFkey)}')">Mark Fixed</button>`;
|
|
1805
|
+
}
|
|
1806
|
+
html += `<button class="gov-action-btn" style="background:#fee2e2;color:#991b1b;border:none;padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer;" onclick="event.stopPropagation();unassignFix('${escapeHtml(ctrlFkey)}')">Unassign</button>`;
|
|
1807
|
+
html += `</div>`;
|
|
1808
|
+
html += `</div>`;
|
|
1809
|
+
if (record) {
|
|
1810
|
+
html += renderProvenanceChainInline(record);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
html += `<div style="padding:12px 16px;border-radius:8px;background:#f9fafb;border:1px dashed #d1d5db;">`;
|
|
1815
|
+
html += `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">`;
|
|
1816
|
+
html += `<div style="font-size:13px;color:#6b7280;">This control is not linked to any governance record. Assign it to create a provenance chain for auditors.</div>`;
|
|
1817
|
+
html += `<button class="gov-action-btn" style="background:#4f46e5;color:#fff;border:none;padding:6px 14px;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600;white-space:nowrap;" onclick="event.stopPropagation();openAssignModal('${escapeHtml(ctrlFkey)}','${escapeHtml(controlId)}','${escapeHtml(controlName.replace(/'/g, "\\'"))}','',0,'${escapeHtml(severity)}','${escapeHtml(controlId)}')">+ Assign to Governance Record</button>`;
|
|
1818
|
+
html += `</div>`;
|
|
1819
|
+
html += `</div>`;
|
|
1820
|
+
if (issueHasFindingLevelAssignments(ctrlFkey, controlId)) {
|
|
1821
|
+
html += `<div style="margin-top:8px;font-size:11px;color:#6b7280;">ℹ Individual audit findings within this control may already be assigned at the finding level below.</div>`;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
html += `</div>`;
|
|
1825
|
+
return html;
|
|
1826
|
+
}
|
|
1827
|
+
function issueHasFindingLevelAssignments(ctrlFkey, controlId) {
|
|
1828
|
+
return false;
|
|
1829
|
+
}
|
|
1830
|
+
function renderProvenanceChainInline(record) {
|
|
1831
|
+
let html = `<div style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;">`;
|
|
1832
|
+
html += `<div style="background:#f3f4f6;padding:10px 14px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;">`;
|
|
1833
|
+
html += `<span style="font-size:14px;font-weight:700;color:#1f2937;">${escapeHtml(record.system_name)}</span>`;
|
|
1834
|
+
html += `<span class="badge" style="background:${record.status === "approved" ? "#22c55e" : record.status === "rejected" || record.status === "revoked" ? "#ef4444" : "#eab308"};color:#fff;font-size:10px;text-transform:uppercase;">${escapeHtml(record.status)}</span>`;
|
|
1835
|
+
html += `<span class="badge" style="background:${record.risk_level === "critical" ? "#ef4444" : record.risk_level === "high" ? "#f97316" : record.risk_level === "medium" ? "#eab308" : "#22c55e"};color:#fff;font-size:10px;text-transform:uppercase;">${escapeHtml(record.risk_level)} RISK</span>`;
|
|
1836
|
+
html += `<span style="font-size:11px;color:#9ca3af;font-family:monospace;">${escapeHtml(record.id)}</span>`;
|
|
1837
|
+
html += `</div>`;
|
|
1838
|
+
html += `<div style="padding:10px 14px;">`;
|
|
1839
|
+
html += `<table style="width:100%;font-size:12px;border-collapse:collapse;">`;
|
|
1840
|
+
if (record.approval) {
|
|
1841
|
+
const a = record.approval;
|
|
1842
|
+
const decColor = a.decision === "approved" ? "#22c55e" : "#ef4444";
|
|
1843
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;">`;
|
|
1844
|
+
html += `<td style="padding:6px 8px;font-weight:600;width:140px;color:#374151;">Approval</td>`;
|
|
1845
|
+
html += `<td style="padding:6px 8px;">`;
|
|
1846
|
+
html += `<span style="color:${decColor};font-weight:600;">${escapeHtml(a.decision.toUpperCase())}</span> by ${escapeHtml(a.approver_name)} (${escapeHtml(a.approver_role)})`;
|
|
1847
|
+
if (a.valid_until)
|
|
1848
|
+
html += ` — valid until ${escapeHtml(a.valid_until)}`;
|
|
1849
|
+
html += `</td></tr>`;
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;"><td style="padding:6px 8px;font-weight:600;width:140px;color:#374151;">Approval</td><td style="padding:6px 8px;color:#ef4444;">✗ Not recorded</td></tr>`;
|
|
1853
|
+
}
|
|
1854
|
+
if (record.risk_assessment) {
|
|
1855
|
+
const ra = record.risk_assessment;
|
|
1856
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;">`;
|
|
1857
|
+
html += `<td style="padding:6px 8px;font-weight:600;color:#374151;">Risk Assessment</td>`;
|
|
1858
|
+
html += `<td style="padding:6px 8px;">Score: <strong>${escapeHtml(ra.risk_score)}</strong> — Residual: ${escapeHtml(ra.residual_risk)} (${escapeHtml(ra.methodology)})</td>`;
|
|
1859
|
+
html += `</tr>`;
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;"><td style="padding:6px 8px;font-weight:600;color:#374151;">Risk Assessment</td><td style="padding:6px 8px;color:#9ca3af;">✗ Not assessed</td></tr>`;
|
|
1863
|
+
}
|
|
1864
|
+
if (record.policy_basis) {
|
|
1865
|
+
const pb = record.policy_basis;
|
|
1866
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;">`;
|
|
1867
|
+
html += `<td style="padding:6px 8px;font-weight:600;color:#374151;">Policy Basis</td>`;
|
|
1868
|
+
html += `<td style="padding:6px 8px;">${escapeHtml(pb.policy_name)} v${escapeHtml(pb.version)} (${escapeHtml(pb.standard)})</td>`;
|
|
1869
|
+
html += `</tr>`;
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;"><td style="padding:6px 8px;font-weight:600;color:#374151;">Policy Basis</td><td style="padding:6px 8px;color:#9ca3af;">✗ Not documented</td></tr>`;
|
|
1873
|
+
}
|
|
1874
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;">`;
|
|
1875
|
+
html += `<td style="padding:6px 8px;font-weight:600;color:#374151;">Evidence Chain</td>`;
|
|
1876
|
+
if (record.evidence.length > 0) {
|
|
1877
|
+
html += `<td style="padding:6px 8px;">`;
|
|
1878
|
+
for (const e of record.evidence) {
|
|
1879
|
+
html += `<span style="display:inline-block;margin-right:6px;margin-bottom:2px;padding:2px 8px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;font-size:11px;">${escapeHtml(e.title)} <span style="color:#6b7280;">(${escapeHtml(e.source_system)}: ${escapeHtml(e.reference)})</span></span>`;
|
|
1880
|
+
}
|
|
1881
|
+
html += `</td>`;
|
|
1882
|
+
}
|
|
1883
|
+
else {
|
|
1884
|
+
html += `<td style="padding:6px 8px;color:#9ca3af;">✗ No evidence references</td>`;
|
|
1885
|
+
}
|
|
1886
|
+
html += `</tr>`;
|
|
1887
|
+
if (record.review_cycle) {
|
|
1888
|
+
const rc = record.review_cycle;
|
|
1889
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;">`;
|
|
1890
|
+
html += `<td style="padding:6px 8px;font-weight:600;color:#374151;">Review Cycle</td>`;
|
|
1891
|
+
html += `<td style="padding:6px 8px;">${escapeHtml(rc.frequency)} — next review: ${escapeHtml(rc.next_review)}</td>`;
|
|
1892
|
+
html += `</tr>`;
|
|
1893
|
+
}
|
|
1894
|
+
else {
|
|
1895
|
+
html += `<tr style="border-bottom:1px solid #f3f4f6;"><td style="padding:6px 8px;font-weight:600;color:#374151;">Review Cycle</td><td style="padding:6px 8px;color:#9ca3af;">✗ Not scheduled</td></tr>`;
|
|
1896
|
+
}
|
|
1897
|
+
html += `<tr>`;
|
|
1898
|
+
html += `<td style="padding:6px 8px;font-weight:600;color:#374151;">Provenance Chain</td>`;
|
|
1899
|
+
const chainParts = [];
|
|
1900
|
+
chainParts.push(record.approval ? "✓" : "✗");
|
|
1901
|
+
chainParts.push(record.risk_assessment ? "✓" : "✗");
|
|
1902
|
+
chainParts.push(record.policy_basis ? "✓" : "✗");
|
|
1903
|
+
chainParts.push(record.evidence.length > 0 ? "✓" : "✗");
|
|
1904
|
+
chainParts.push(record.review_cycle ? "✓" : "✗");
|
|
1905
|
+
html += `<td style="padding:6px 8px;font-size:11px;">Approval ${chainParts[0]} → Risk ${chainParts[1]} → Policy ${chainParts[2]} → Evidence ${chainParts[3]} → Review ${chainParts[4]}</td>`;
|
|
1906
|
+
html += `</tr>`;
|
|
1907
|
+
html += `</table>`;
|
|
1908
|
+
html += `</div>`;
|
|
1909
|
+
html += `</div>`;
|
|
1910
|
+
return html;
|
|
1911
|
+
}
|
|
1613
1912
|
function renderActivityLogSection(entries) {
|
|
1614
1913
|
if (!entries || entries.length === 0) {
|
|
1615
1914
|
return `<div class="card">
|
|
@@ -1886,8 +2185,8 @@ function renderGovernanceSection(data) {
|
|
|
1886
2185
|
html += `<div><strong>Decision:</strong> <span style="color:${a.decision === "approved" ? "#22c55e" : "#ef4444"};font-weight:600;">${a.decision.toUpperCase()}</span></div>`;
|
|
1887
2186
|
html += `<div><strong>Date:</strong> ${escapeHtml(a.decision_date)}</div>`;
|
|
1888
2187
|
html += `<div><strong>Validity:</strong> ${escapeHtml(a.valid_from)} → ${escapeHtml(a.valid_until || "indefinite")}</div>`;
|
|
1889
|
-
if (a.conditions.length > 0) {
|
|
1890
|
-
html += `<div><strong>Conditions:</strong> ${a.conditions.map(c => escapeHtml(c)).join("; ")}</div>`;
|
|
2188
|
+
if (a.conditions && a.conditions.length > 0) {
|
|
2189
|
+
html += `<div><strong>Conditions:</strong> ${(a.conditions || []).map(c => escapeHtml(c)).join("; ")}</div>`;
|
|
1891
2190
|
}
|
|
1892
2191
|
if (a.rationale) {
|
|
1893
2192
|
html += `<div><strong>Rationale:</strong> ${escapeHtml(a.rationale)}</div>`;
|
|
@@ -1906,8 +2205,8 @@ function renderGovernanceSection(data) {
|
|
|
1906
2205
|
html += `<div><strong>Methodology:</strong> ${escapeHtml(ra.methodology)}</div>`;
|
|
1907
2206
|
html += `<div><strong>Risk Score:</strong> ${escapeHtml(ra.risk_score)} — <strong>Residual:</strong> ${escapeHtml(ra.residual_risk)}</div>`;
|
|
1908
2207
|
html += `<div><strong>Date:</strong> ${escapeHtml(ra.assessment_date)}</div>`;
|
|
1909
|
-
if (ra.identified_risks.length > 0) {
|
|
1910
|
-
html += `<div><strong>Identified Risks:</strong> ${ra.identified_risks.map(r => escapeHtml(r)).join(", ")}</div>`;
|
|
2208
|
+
if (ra.identified_risks && ra.identified_risks.length > 0) {
|
|
2209
|
+
html += `<div><strong>Identified Risks:</strong> ${(ra.identified_risks || []).map(r => escapeHtml(r)).join(", ")}</div>`;
|
|
1911
2210
|
}
|
|
1912
2211
|
html += `</div>`;
|
|
1913
2212
|
html += `</div>`;
|
|
@@ -1918,8 +2217,8 @@ function renderGovernanceSection(data) {
|
|
|
1918
2217
|
html += `<div style="font-size:13px;line-height:1.8;">`;
|
|
1919
2218
|
html += `<div><strong>Policy:</strong> ${escapeHtml(pb.policy_name)} (${escapeHtml(pb.policy_id)} v${escapeHtml(pb.version)})</div>`;
|
|
1920
2219
|
html += `<div><strong>Standard:</strong> ${escapeHtml(pb.standard)}</div>`;
|
|
1921
|
-
if (pb.clauses.length > 0) {
|
|
1922
|
-
html += `<div><strong>Clauses:</strong> ${pb.clauses.map(c => escapeHtml(c)).join(", ")}</div>`;
|
|
2220
|
+
if (pb.clauses && pb.clauses.length > 0) {
|
|
2221
|
+
html += `<div><strong>Clauses:</strong> ${(pb.clauses || []).map(c => escapeHtml(c)).join(", ")}</div>`;
|
|
1923
2222
|
}
|
|
1924
2223
|
html += `</div>`;
|
|
1925
2224
|
html += `</div>`;
|
|
@@ -1957,7 +2256,7 @@ function renderGovernanceSection(data) {
|
|
|
1957
2256
|
html += `<div><strong>Committee:</strong> ${escapeHtml(c.committee_name)}</div>`;
|
|
1958
2257
|
html += `<div><strong>Meeting:</strong> ${escapeHtml(c.meeting_date)} (${escapeHtml(c.meeting_reference)})</div>`;
|
|
1959
2258
|
if (c.attendees.length > 0) {
|
|
1960
|
-
html += `<div><strong>Attendees:</strong> ${c.attendees.map(a => escapeHtml(a)).join(", ")}</div>`;
|
|
2259
|
+
html += `<div><strong>Attendees:</strong> ${(c.attendees || []).map(a => escapeHtml(a)).join(", ")}</div>`;
|
|
1961
2260
|
}
|
|
1962
2261
|
html += `</div>`;
|
|
1963
2262
|
html += `</div>`;
|
|
@@ -1983,7 +2282,9 @@ function renderGovernanceSection(data) {
|
|
|
1983
2282
|
return html;
|
|
1984
2283
|
}
|
|
1985
2284
|
function escapeHtml(str) {
|
|
1986
|
-
|
|
2285
|
+
if (str === null || str === undefined)
|
|
2286
|
+
return "";
|
|
2287
|
+
return String(str)
|
|
1987
2288
|
.replace(/&/g, "&")
|
|
1988
2289
|
.replace(/</g, "<")
|
|
1989
2290
|
.replace(/>/g, ">")
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"@greenarmor/ges-audit-engine": "1.
|
|
4
|
-
"@greenarmor/ges-core": "1.
|
|
5
|
-
"@greenarmor/ges-policy-engine": "1.
|
|
6
|
-
"@greenarmor/ges-report-generator": "1.
|
|
7
|
-
"@greenarmor/ges-scoring-engine": "1.
|
|
3
|
+
"@greenarmor/ges-audit-engine": "1.5.1",
|
|
4
|
+
"@greenarmor/ges-core": "1.5.1",
|
|
5
|
+
"@greenarmor/ges-policy-engine": "1.5.1",
|
|
6
|
+
"@greenarmor/ges-report-generator": "1.5.1",
|
|
7
|
+
"@greenarmor/ges-scoring-engine": "1.5.1"
|
|
8
8
|
},
|
|
9
9
|
"description": "GESF Web Dashboard - Visual compliance dashboard for teams",
|
|
10
10
|
"devDependencies": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"type": "module",
|
|
43
43
|
"types": "./dist/index.d.ts",
|
|
44
|
-
"version": "1.
|
|
44
|
+
"version": "1.5.1",
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc",
|
|
47
47
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|