@greenarmor/ges-web-dashboard 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +129 -1
- package/dist/template.js +165 -5
- 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.0",
|
|
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>`;
|
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.0",
|
|
4
|
+
"@greenarmor/ges-core": "1.5.0",
|
|
5
|
+
"@greenarmor/ges-policy-engine": "1.5.0",
|
|
6
|
+
"@greenarmor/ges-report-generator": "1.5.0",
|
|
7
|
+
"@greenarmor/ges-scoring-engine": "1.5.0"
|
|
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.0",
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc",
|
|
47
47
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|