@greenarmor/ges-web-dashboard 1.5.0 → 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.js CHANGED
@@ -195,7 +195,7 @@ export function collectDashboardData(projectPath) {
195
195
  projectName: config?.project_name || "Unknown Project",
196
196
  projectType: config?.project_type || "unknown",
197
197
  frameworks: allFrameworks,
198
- gesfVersion: "1.5.0",
198
+ gesfVersion: "1.5.1",
199
199
  score,
200
200
  controls,
201
201
  findings,
package/dist/template.js CHANGED
@@ -1754,6 +1754,9 @@ function renderComplianceFixCards(issues, idPrefix, assignmentMap, govRecords) {
1754
1754
  }
1755
1755
  }
1756
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);
1757
1760
  html += `<div class="fix-section"><div class="fix-section-title">Traceability</div>`;
1758
1761
  html += `<table><tbody>`;
1759
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> &mdash; ${escapeHtml(issue.controlName)}</td></tr>`;
@@ -1770,6 +1773,142 @@ function renderComplianceFixCards(issues, idPrefix, assignmentMap, govRecords) {
1770
1773
  }
1771
1774
  return html;
1772
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>&#10003; 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;">&#8505; 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 += ` &mdash; 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;">&#10007; 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> &mdash; 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;">&#10007; 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;">&#10007; 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;">&#10007; 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)} &mdash; 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;">&#10007; 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 ? "&#10003;" : "&#10007;");
1901
+ chainParts.push(record.risk_assessment ? "&#10003;" : "&#10007;");
1902
+ chainParts.push(record.policy_basis ? "&#10003;" : "&#10007;");
1903
+ chainParts.push(record.evidence.length > 0 ? "&#10003;" : "&#10007;");
1904
+ chainParts.push(record.review_cycle ? "&#10003;" : "&#10007;");
1905
+ html += `<td style="padding:6px 8px;font-size:11px;">Approval ${chainParts[0]} &rarr; Risk ${chainParts[1]} &rarr; Policy ${chainParts[2]} &rarr; Evidence ${chainParts[3]} &rarr; Review ${chainParts[4]}</td>`;
1906
+ html += `</tr>`;
1907
+ html += `</table>`;
1908
+ html += `</div>`;
1909
+ html += `</div>`;
1910
+ return html;
1911
+ }
1773
1912
  function renderActivityLogSection(entries) {
1774
1913
  if (!entries || entries.length === 0) {
1775
1914
  return `<div class="card">
@@ -2046,8 +2185,8 @@ function renderGovernanceSection(data) {
2046
2185
  html += `<div><strong>Decision:</strong> <span style="color:${a.decision === "approved" ? "#22c55e" : "#ef4444"};font-weight:600;">${a.decision.toUpperCase()}</span></div>`;
2047
2186
  html += `<div><strong>Date:</strong> ${escapeHtml(a.decision_date)}</div>`;
2048
2187
  html += `<div><strong>Validity:</strong> ${escapeHtml(a.valid_from)} &rarr; ${escapeHtml(a.valid_until || "indefinite")}</div>`;
2049
- if (a.conditions.length > 0) {
2050
- 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>`;
2051
2190
  }
2052
2191
  if (a.rationale) {
2053
2192
  html += `<div><strong>Rationale:</strong> ${escapeHtml(a.rationale)}</div>`;
@@ -2066,8 +2205,8 @@ function renderGovernanceSection(data) {
2066
2205
  html += `<div><strong>Methodology:</strong> ${escapeHtml(ra.methodology)}</div>`;
2067
2206
  html += `<div><strong>Risk Score:</strong> ${escapeHtml(ra.risk_score)} &mdash; <strong>Residual:</strong> ${escapeHtml(ra.residual_risk)}</div>`;
2068
2207
  html += `<div><strong>Date:</strong> ${escapeHtml(ra.assessment_date)}</div>`;
2069
- if (ra.identified_risks.length > 0) {
2070
- 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>`;
2071
2210
  }
2072
2211
  html += `</div>`;
2073
2212
  html += `</div>`;
@@ -2078,8 +2217,8 @@ function renderGovernanceSection(data) {
2078
2217
  html += `<div style="font-size:13px;line-height:1.8;">`;
2079
2218
  html += `<div><strong>Policy:</strong> ${escapeHtml(pb.policy_name)} (${escapeHtml(pb.policy_id)} v${escapeHtml(pb.version)})</div>`;
2080
2219
  html += `<div><strong>Standard:</strong> ${escapeHtml(pb.standard)}</div>`;
2081
- if (pb.clauses.length > 0) {
2082
- 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>`;
2083
2222
  }
2084
2223
  html += `</div>`;
2085
2224
  html += `</div>`;
@@ -2117,7 +2256,7 @@ function renderGovernanceSection(data) {
2117
2256
  html += `<div><strong>Committee:</strong> ${escapeHtml(c.committee_name)}</div>`;
2118
2257
  html += `<div><strong>Meeting:</strong> ${escapeHtml(c.meeting_date)} (${escapeHtml(c.meeting_reference)})</div>`;
2119
2258
  if (c.attendees.length > 0) {
2120
- 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>`;
2121
2260
  }
2122
2261
  html += `</div>`;
2123
2262
  html += `</div>`;
@@ -2143,7 +2282,9 @@ function renderGovernanceSection(data) {
2143
2282
  return html;
2144
2283
  }
2145
2284
  function escapeHtml(str) {
2146
- return str
2285
+ if (str === null || str === undefined)
2286
+ return "";
2287
+ return String(str)
2147
2288
  .replace(/&/g, "&amp;")
2148
2289
  .replace(/</g, "&lt;")
2149
2290
  .replace(/>/g, "&gt;")
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "dependencies": {
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"
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.5.0",
44
+ "version": "1.5.1",
45
45
  "scripts": {
46
46
  "build": "tsc",
47
47
  "clean": "rm -rf dist tsconfig.tsbuildinfo",