|
| 1 | +{ |
| 2 | + "_metadata": { |
| 3 | + "name": "Container Image CVE Triage", |
| 4 | + "description": "Supply-chain research workflow that scans a public container image with Trivy, ranks fixable CVEs, and writes an actionable image upgrade report.", |
| 5 | + "category": "container-security", |
| 6 | + "tags": ["container", "image", "cve", "trivy", "supply-chain"], |
| 7 | + "author": "sentris-team", |
| 8 | + "version": "1.0.0" |
| 9 | + }, |
| 10 | + "manifest": { |
| 11 | + "name": "Container Image CVE Triage", |
| 12 | + "description": "Scan container images for known CVEs and produce a prioritized remediation report.", |
| 13 | + "version": "1.0.0", |
| 14 | + "author": "sentris-team", |
| 15 | + "category": "container-security", |
| 16 | + "tags": ["container", "image", "cve", "trivy", "supply-chain"], |
| 17 | + "entryPoint": "trigger_1", |
| 18 | + "nodeCount": 4, |
| 19 | + "edgeCount": 7 |
| 20 | + }, |
| 21 | + "graph": { |
| 22 | + "name": "Container Image CVE Triage", |
| 23 | + "description": "Container image CVE triage workflow for authorized supply-chain and deployment research.", |
| 24 | + "nodes": [ |
| 25 | + { |
| 26 | + "id": "trigger_1", |
| 27 | + "type": "core.workflow.entrypoint", |
| 28 | + "position": { "x": 100, "y": 260 }, |
| 29 | + "data": { |
| 30 | + "label": "Container Image Input", |
| 31 | + "config": { |
| 32 | + "params": { |
| 33 | + "runtimeInputs": [ |
| 34 | + { |
| 35 | + "id": "imageRef", |
| 36 | + "label": "Container image reference", |
| 37 | + "type": "text", |
| 38 | + "required": true, |
| 39 | + "description": "Public or locally available container image reference, for example nginx:1.25 or node:18-alpine." |
| 40 | + }, |
| 41 | + { |
| 42 | + "id": "deploymentContext", |
| 43 | + "label": "Deployment context", |
| 44 | + "type": "text", |
| 45 | + "required": false, |
| 46 | + "defaultValue": "", |
| 47 | + "description": "Where the image is used, exposure level, or business context for ranking CVE urgency." |
| 48 | + }, |
| 49 | + { |
| 50 | + "id": "authorizationNotes", |
| 51 | + "label": "Authorization notes", |
| 52 | + "type": "text", |
| 53 | + "required": false, |
| 54 | + "defaultValue": "", |
| 55 | + "description": "Program scope, ownership notes, or testing constraints to include in the report." |
| 56 | + } |
| 57 | + ] |
| 58 | + }, |
| 59 | + "inputOverrides": {} |
| 60 | + } |
| 61 | + } |
| 62 | + }, |
| 63 | + { |
| 64 | + "id": "trivy_image_scan", |
| 65 | + "type": "sentris.trivy.run", |
| 66 | + "position": { "x": 420, "y": 260 }, |
| 67 | + "data": { |
| 68 | + "label": "Scan Image CVEs", |
| 69 | + "config": { |
| 70 | + "params": { |
| 71 | + "scanType": "image", |
| 72 | + "severity": ["CRITICAL", "HIGH", "MEDIUM"], |
| 73 | + "format": "json" |
| 74 | + }, |
| 75 | + "inputOverrides": {} |
| 76 | + } |
| 77 | + } |
| 78 | + }, |
| 79 | + { |
| 80 | + "id": "assemble_image_cve_report", |
| 81 | + "type": "core.logic.script", |
| 82 | + "position": { "x": 760, "y": 260 }, |
| 83 | + "data": { |
| 84 | + "label": "Rank Image CVEs", |
| 85 | + "config": { |
| 86 | + "params": { |
| 87 | + "variables": [ |
| 88 | + { "name": "imageRef", "type": "string" }, |
| 89 | + { "name": "deploymentContext", "type": "string" }, |
| 90 | + { "name": "authorizationNotes", "type": "string" }, |
| 91 | + { "name": "vulnerabilities", "type": "list-json" }, |
| 92 | + { "name": "vulnerabilityCount", "type": "number" } |
| 93 | + ], |
| 94 | + "returns": [{ "name": "report", "type": "json" }], |
| 95 | + "code": "export function script(input) {\n const vulnerabilities = Array.isArray(input.vulnerabilities) ? input.vulnerabilities : [];\n const severityRank = { critical: 4, high: 3, medium: 2, low: 1, info: 0, unknown: 0 };\n const normalizeSeverity = (value) => String(value || 'unknown').toLowerCase();\n const rankFor = (value) => severityRank[normalizeSeverity(value)] ?? 0;\n const hasFix = (finding) => typeof finding?.fixedVersion === 'string' && finding.fixedVersion.trim().length > 0;\n const priorityBandFor = (finding) => {\n const rank = rankFor(finding?.severity);\n if (rank >= 4) return 'immediate';\n if (rank >= 3) return hasFix(finding) ? 'high-fix-available' : 'high-review';\n if (rank >= 2) return hasFix(finding) ? 'scheduled-fix' : 'watchlist';\n return 'backlog';\n };\n const reasonsFor = (finding) => {\n const severity = normalizeSeverity(finding?.severity);\n const reasons = [`${severity} severity`];\n if (hasFix(finding)) reasons.push('fixed version available');\n if (/openssl|libssl|gnutls|curl|nginx|apache|node|java|glibc|busybox/i.test(String(finding?.pkgName || ''))) reasons.push('security-sensitive package');\n if (/internet|external|public|edge|reverse proxy|api/i.test(String(input.deploymentContext || ''))) reasons.push('deployment context suggests external exposure');\n return reasons;\n };\n const priorityFindings = vulnerabilities\n .map((finding) => {\n const rank = rankFor(finding?.severity);\n const fixBoost = hasFix(finding) ? 25 : 0;\n const exposureBoost = /internet|external|public|edge|reverse proxy|api/i.test(String(input.deploymentContext || '')) ? 10 : 0;\n const packageBoost = /openssl|libssl|gnutls|curl|nginx|apache|node|java|glibc|busybox/i.test(String(finding?.pkgName || '')) ? 8 : 0;\n return {\n vulnerabilityId: finding?.vulnerabilityId,\n pkgName: finding?.pkgName,\n installedVersion: finding?.installedVersion,\n fixedVersion: finding?.fixedVersion,\n severity: normalizeSeverity(finding?.severity),\n title: finding?.title,\n description: finding?.description,\n primaryUrl: finding?.primaryUrl,\n priorityScore: rank * 100 + fixBoost + exposureBoost + packageBoost,\n priorityBand: priorityBandFor(finding),\n priorityReasons: reasonsFor(finding)\n };\n })\n .filter((finding) => finding.vulnerabilityId && rankFor(finding.severity) >= 2)\n .sort((a, b) => b.priorityScore - a.priorityScore || String(a.pkgName || '').localeCompare(String(b.pkgName || '')))\n .slice(0, 100);\n const countsBySeverity = priorityFindings.reduce((acc, finding) => {\n acc[finding.severity] = (acc[finding.severity] || 0) + 1;\n return acc;\n }, {});\n const highestSeverity = priorityFindings.reduce((current, finding) => rankFor(finding.severity) > rankFor(current) ? finding.severity : current, 'none');\n const fixableFindings = priorityFindings.filter((finding) => typeof finding.fixedVersion === 'string' && finding.fixedVersion.trim().length > 0).length;\n return {\n report: {\n summary: {\n imageRef: input.imageRef || null,\n vulnerabilityCount: Number(input.vulnerabilityCount || vulnerabilities.length),\n actionableFindings: priorityFindings.length,\n fixableFindings,\n highestSeverity,\n countsBySeverity\n },\n deploymentContext: input.deploymentContext || null,\n authorizationNotes: input.authorizationNotes || null,\n priorityFindings,\n nextSteps: [\n 'Prioritize immediate and high-fix-available findings first',\n 'Upgrade the base image or affected OS package when fixedVersion is listed',\n 'Confirm whether the vulnerable package is reachable in the deployed image path',\n 'Rebuild and rescan the image after remediation'\n ]\n }\n };\n}" |
| 96 | + }, |
| 97 | + "inputOverrides": {} |
| 98 | + } |
| 99 | + } |
| 100 | + }, |
| 101 | + { |
| 102 | + "id": "artifact_report", |
| 103 | + "type": "core.artifact.writer", |
| 104 | + "position": { "x": 1100, "y": 260 }, |
| 105 | + "data": { |
| 106 | + "label": "Save Image CVE Report", |
| 107 | + "config": { |
| 108 | + "params": { |
| 109 | + "fileExtension": ".json", |
| 110 | + "mimeType": "application/json", |
| 111 | + "saveToRunArtifacts": true, |
| 112 | + "publishToArtifactLibrary": false |
| 113 | + }, |
| 114 | + "inputOverrides": { |
| 115 | + "artifactName": "container-image-cve-triage-{{date}}" |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + ], |
| 121 | + "edges": [ |
| 122 | + { |
| 123 | + "id": "trigger_1-trivy_image_scan-imageRef", |
| 124 | + "source": "trigger_1", |
| 125 | + "target": "trivy_image_scan", |
| 126 | + "sourceHandle": "imageRef", |
| 127 | + "targetHandle": "target" |
| 128 | + }, |
| 129 | + { |
| 130 | + "id": "trigger_1-assemble_image_cve_report-imageRef", |
| 131 | + "source": "trigger_1", |
| 132 | + "target": "assemble_image_cve_report", |
| 133 | + "sourceHandle": "imageRef", |
| 134 | + "targetHandle": "imageRef" |
| 135 | + }, |
| 136 | + { |
| 137 | + "id": "trigger_1-assemble_image_cve_report-deploymentContext", |
| 138 | + "source": "trigger_1", |
| 139 | + "target": "assemble_image_cve_report", |
| 140 | + "sourceHandle": "deploymentContext", |
| 141 | + "targetHandle": "deploymentContext" |
| 142 | + }, |
| 143 | + { |
| 144 | + "id": "trigger_1-assemble_image_cve_report-authorizationNotes", |
| 145 | + "source": "trigger_1", |
| 146 | + "target": "assemble_image_cve_report", |
| 147 | + "sourceHandle": "authorizationNotes", |
| 148 | + "targetHandle": "authorizationNotes" |
| 149 | + }, |
| 150 | + { |
| 151 | + "id": "trivy_image_scan-assemble_image_cve_report-vulnerabilities", |
| 152 | + "source": "trivy_image_scan", |
| 153 | + "target": "assemble_image_cve_report", |
| 154 | + "sourceHandle": "vulnerabilities", |
| 155 | + "targetHandle": "vulnerabilities" |
| 156 | + }, |
| 157 | + { |
| 158 | + "id": "trivy_image_scan-assemble_image_cve_report-vulnerabilityCount", |
| 159 | + "source": "trivy_image_scan", |
| 160 | + "target": "assemble_image_cve_report", |
| 161 | + "sourceHandle": "vulnerabilityCount", |
| 162 | + "targetHandle": "vulnerabilityCount" |
| 163 | + }, |
| 164 | + { |
| 165 | + "id": "assemble_image_cve_report-artifact_report-report", |
| 166 | + "source": "assemble_image_cve_report", |
| 167 | + "target": "artifact_report", |
| 168 | + "sourceHandle": "report", |
| 169 | + "targetHandle": "content" |
| 170 | + } |
| 171 | + ] |
| 172 | + }, |
| 173 | + "requiredSecrets": [] |
| 174 | +} |
0 commit comments