@docupace/pipeline 1.0.121 → 1.0.122

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.
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Renders a self-contained HTML report from `jf` simple-json output.
4
+ #
5
+ # The jf CLI only emits table/json/simple-json/sarif/cyclonedx — there is no
6
+ # HTML or PDF format — so we build one from simple-json.
7
+ #
8
+ # Usage: xray-html-report.sh <output.html> <label:file.json> [label:file.json ...]
9
+ # e.g. xray-html-report.sh report.html "Dependencies:audit.json" "Image:docker.json"
10
+ #
11
+ # Missing or unparseable inputs are reported inside the HTML rather than
12
+ # failing the build.
13
+
14
+ set -uo pipefail
15
+
16
+ OUT="${1:?usage: xray-html-report.sh <output.html> <label:file.json> ...}"
17
+ shift
18
+
19
+ PROJECT="${BITBUCKET_REPO_SLUG:-unknown}"
20
+ COMMIT="${BITBUCKET_COMMIT:-unknown}"
21
+ BRANCH="${BITBUCKET_BRANCH:-unknown}"
22
+ BUILD_URL=""
23
+ if [[ -n "${BITBUCKET_WORKSPACE:-}" && -n "${BITBUCKET_REPO_SLUG:-}" && -n "${BITBUCKET_BUILD_NUMBER:-}" ]]; then
24
+ BUILD_URL="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pipelines/results/${BITBUCKET_BUILD_NUMBER}"
25
+ fi
26
+ GENERATED="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
27
+
28
+ if ! command -v jq >/dev/null 2>&1; then
29
+ echo "jq is required to build the HTML report" >&2
30
+ exit 1
31
+ fi
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Flatten every input into one array of normalized findings.
35
+ # SCA rows (vulnerabilities/violations) and source-code rows (secrets, sast,
36
+ # iac) have different shapes, so each is mapped to a common set of fields.
37
+ # ---------------------------------------------------------------------------
38
+ # Emits one finding per line (JSONL). Passing the whole set through --argjson
39
+ # instead would blow Linux's 128 KB per-argument limit on a real scan.
40
+ build_findings() {
41
+ local label="$1" file="$2"
42
+ if [[ ! -s "$file" ]] || ! jq empty "$file" >/dev/null 2>&1; then
43
+ return 0
44
+ fi
45
+ jq -c --arg scan "$label" '
46
+ def sev_rank:
47
+ if . == "Critical" then 0
48
+ elif . == "High" then 1
49
+ elif . == "Medium" then 2
50
+ elif . == "Low" then 3
51
+ elif . == "Unknown" then 4
52
+ else 5 end;
53
+
54
+ def sca_rows(rows; kind):
55
+ (rows // [])[] | {
56
+ scan: $scan,
57
+ kind: kind,
58
+ severity: (.severity // "Unknown"),
59
+ rank: ((.severity // "Unknown") | sev_rank),
60
+ id: ((.cves // [] | map(.cve // empty) | join(", ")) as $c
61
+ | if $c == "" then (.issueId // "N/A") else $c end),
62
+ cves: (.cves // [] | map(.cve // empty)),
63
+ cvss: ((.cves // [] | map(.cvssV3 // empty) | first) // ""),
64
+ package: ((.impactedPackageName // "N/A") + " " + (.impactedPackageVersion // "")),
65
+ type: (.impactedPackageType // ""),
66
+ fixed: (.fixedVersions // [] | join(", ")),
67
+ applicable: (.applicable // ""),
68
+ summary: (.summary // ""),
69
+ direct: ((.components // []) | map((.name // "") + " " + (.version // "")) | unique | join(", ")),
70
+ location: "",
71
+ watch: (.watch // ""),
72
+ policies: (.policies // [] | join(", "))
73
+ };
74
+
75
+ def code_rows(rows; kind):
76
+ (rows // [])[] | {
77
+ scan: $scan,
78
+ kind: kind,
79
+ severity: (.severity // "Unknown"),
80
+ rank: ((.severity // "Unknown") | sev_rank),
81
+ id: (.ruleId // "N/A"),
82
+ cves: [],
83
+ cvss: "",
84
+ package: (.finding // .scannerShortDescription // "N/A"),
85
+ type: ((.cwe // []) | join(", ")),
86
+ fixed: "",
87
+ applicable: "",
88
+ summary: (.scannerDescription // .scannerShortDescription // ""),
89
+ direct: "",
90
+ location: ((.file // "") as $f
91
+ | if $f == "" then "" else $f + (if (.startLine // 0) > 0 then ":" + ((.startLine) | tostring) else "" end) end),
92
+ watch: (.watch // ""),
93
+ policies: (.policies // [] | join(", "))
94
+ };
95
+
96
+ [ sca_rows(.vulnerabilities; "Vulnerability")
97
+ , sca_rows(.securityViolations; "Security violation")
98
+ , sca_rows(.licensesViolations; "License violation")
99
+ , sca_rows(.operationalRiskViolations; "Operational risk")
100
+ , code_rows(.secrets; "Secret")
101
+ , code_rows(.secretsViolations; "Secret violation")
102
+ , code_rows(.sast; "SAST")
103
+ , code_rows(.sastViolations; "SAST violation")
104
+ , code_rows(.iac; "IaC")
105
+ , code_rows(.iacViolations; "IaC violation")
106
+ , code_rows(.maliciousCode; "Malicious code")
107
+ ] | flatten | .[]
108
+ ' "$file" 2>/dev/null
109
+ }
110
+
111
+ TMP_FINDINGS="$(mktemp)"
112
+ TMP_SOURCES="$(mktemp)"
113
+ trap 'rm -f "$TMP_FINDINGS" "$TMP_SOURCES"' EXIT
114
+ : > "$TMP_FINDINGS"
115
+
116
+ for arg in "$@"; do
117
+ label="${arg%%:*}"
118
+ file="${arg#*:}"
119
+
120
+ status="ok"
121
+ if [[ ! -f "$file" ]]; then
122
+ status="not produced"
123
+ elif [[ ! -s "$file" ]]; then
124
+ status="empty"
125
+ elif ! jq empty "$file" >/dev/null 2>&1; then
126
+ status="unparseable"
127
+ fi
128
+ # Keys are quoted because `label` is a reserved word in jq's grammar — as a
129
+ # bare key it fails to compile ("unexpected label, expecting IDENT").
130
+ jq -cn --arg lbl "$label" --arg file "$file" --arg status "$status" \
131
+ '{"label":$lbl, "file":$file, "status":$status}' >> "$TMP_SOURCES"
132
+
133
+ if [[ "$status" == "ok" ]]; then
134
+ build_findings "$label" "$file" >> "$TMP_FINDINGS"
135
+ fi
136
+ done
137
+
138
+ count_sev() { jq -s --arg s "$1" '[.[]|select(.severity==$s)]|length' "$TMP_FINDINGS"; }
139
+ TOTAL=$(jq -s 'length' "$TMP_FINDINGS")
140
+ CRITICAL=$(count_sev Critical)
141
+ HIGH=$(count_sev High)
142
+ MEDIUM=$(count_sev Medium)
143
+ LOW=$(count_sev Low)
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Render. jq's @html escapes every interpolated value.
147
+ # ---------------------------------------------------------------------------
148
+ jq -s -r \
149
+ --arg project "$PROJECT" \
150
+ --arg commit "$COMMIT" \
151
+ --arg branch "$BRANCH" \
152
+ --arg buildurl "$BUILD_URL" \
153
+ --arg generated "$GENERATED" \
154
+ --argjson total "$TOTAL" \
155
+ --argjson critical "$CRITICAL" \
156
+ --argjson high "$HIGH" \
157
+ --argjson medium "$MEDIUM" \
158
+ --argjson low "$LOW" \
159
+ --slurpfile sources "$TMP_SOURCES" '
160
+ def h: . // "" | tostring | @html;
161
+ def cls: if . == "Critical" then "critical"
162
+ elif . == "High" then "high"
163
+ elif . == "Medium" then "medium"
164
+ elif . == "Low" then "low"
165
+ else "unknown" end;
166
+
167
+ "<!DOCTYPE html>",
168
+ "<html lang=\"en\"><head><meta charset=\"utf-8\">",
169
+ "<title>Xray scan — \($project|h) @ \($commit[0:7]|h)</title>",
170
+ "<style>",
171
+ ":root{--crit:#b3121f;--high:#e8590c;--med:#f0a500;--low:#4c6ef5;--unk:#868e96}",
172
+ "*{box-sizing:border-box}",
173
+ "body{font:14px/1.5 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;margin:0;padding:32px;color:#212529;background:#fff}",
174
+ "h1{font-size:22px;margin:0 0 4px}h2{font-size:16px;margin:32px 0 8px;padding-bottom:6px;border-bottom:2px solid #e9ecef}",
175
+ ".meta{color:#666;font-size:13px;margin-bottom:24px}",
176
+ ".meta code{background:#f1f3f5;padding:1px 5px;border-radius:3px}",
177
+ ".cards{display:flex;gap:12px;flex-wrap:wrap;margin:16px 0 8px}",
178
+ ".card{border:1px solid #dee2e6;border-radius:6px;padding:12px 18px;min-width:104px}",
179
+ ".card .n{font-size:26px;font-weight:600;line-height:1.1}",
180
+ ".card .l{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:#666}",
181
+ ".card.critical{border-top:3px solid var(--crit)}.card.critical .n{color:var(--crit)}",
182
+ ".card.high{border-top:3px solid var(--high)}.card.high .n{color:var(--high)}",
183
+ ".card.medium{border-top:3px solid var(--med)}.card.medium .n{color:var(--med)}",
184
+ ".card.low{border-top:3px solid var(--low)}.card.low .n{color:var(--low)}",
185
+ "table{border-collapse:collapse;width:100%;margin-top:8px;font-size:13px}",
186
+ "th,td{text-align:left;padding:7px 9px;border-bottom:1px solid #e9ecef;vertical-align:top}",
187
+ "th{background:#f8f9fa;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:#495057;position:sticky;top:0}",
188
+ "tr:hover td{background:#fcfcfd}",
189
+ ".sev{font-weight:600;white-space:nowrap}",
190
+ ".sev.critical{color:var(--crit)}.sev.high{color:var(--high)}.sev.medium{color:var(--med)}.sev.low{color:var(--low)}.sev.unknown{color:var(--unk)}",
191
+ ".pkg{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all}",
192
+ ".sum{color:#555;max-width:460px}",
193
+ ".none{padding:16px;background:#ebfbee;border:1px solid #b7eb8f;border-radius:6px;color:#2b8a3e}",
194
+ ".warn{padding:12px;background:#fff9db;border:1px solid #ffe066;border-radius:6px;margin-bottom:16px}",
195
+ "footer{margin-top:40px;color:#868e96;font-size:12px;border-top:1px solid #e9ecef;padding-top:12px}",
196
+ "@media print{body{padding:0}th{position:static}.card{break-inside:avoid}tr{break-inside:avoid}}",
197
+ "</style></head><body>",
198
+
199
+ "<h1>JFrog Xray scan report</h1>",
200
+ "<div class=\"meta\">",
201
+ "<strong>\($project|h)</strong> &middot; branch <code>\($branch|h)</code> &middot; commit <code>\($commit[0:12]|h)</code><br>",
202
+ "Generated \($generated|h)",
203
+ (if $buildurl != "" then " &middot; <a href=\"\($buildurl|h)\">pipeline run</a>" else "" end),
204
+ "</div>",
205
+
206
+ "<div class=\"cards\">",
207
+ "<div class=\"card critical\"><div class=\"n\">\($critical)</div><div class=\"l\">Critical</div></div>",
208
+ "<div class=\"card high\"><div class=\"n\">\($high)</div><div class=\"l\">High</div></div>",
209
+ "<div class=\"card medium\"><div class=\"n\">\($medium)</div><div class=\"l\">Medium</div></div>",
210
+ "<div class=\"card low\"><div class=\"n\">\($low)</div><div class=\"l\">Low</div></div>",
211
+ "<div class=\"card\"><div class=\"n\">\($total)</div><div class=\"l\">Total</div></div>",
212
+ "</div>",
213
+
214
+ # Flag scans that produced nothing, so an empty report is never mistaken
215
+ # for a clean one.
216
+ ( $sources | map(select(.status != "ok")) ) as $bad
217
+ | if ($bad | length) > 0 then
218
+ "<div class=\"warn\"><strong>Incomplete scan data.</strong> "
219
+ + ($bad | map("\(.label) (\(.file)): \(.status)") | join("; ") | @html)
220
+ + " — findings below may be understated.</div>"
221
+ else empty end,
222
+
223
+ if $total == 0 then
224
+ "<div class=\"none\">No findings reported across "
225
+ + ($sources | map(select(.status=="ok")) | length | tostring)
226
+ + " scan(s).</div>"
227
+ else
228
+ ( [ .[] ] | group_by(.scan) | .[] |
229
+ "<h2>\(.[0].scan|h) — \(length) finding(s)</h2>",
230
+ "<table><thead><tr><th>Severity</th><th>Kind</th><th>ID</th><th>Component / finding</th><th>Fixed in</th><th>Details</th></tr></thead><tbody>",
231
+ ( sort_by(.rank, .package) | .[] |
232
+ "<tr>",
233
+ "<td class=\"sev \(.severity|cls)\">\(.severity|h)</td>",
234
+ "<td>\(.kind|h)</td>",
235
+ "<td class=\"pkg\">"
236
+ + ( if (.cves|length) > 0 then
237
+ (.cves | map("<a href=\"https://nvd.nist.gov/vuln/detail/\(.|@uri)\">\(.|@html)</a>") | join("<br>"))
238
+ else (.id|h) end )
239
+ + "</td>",
240
+ "<td class=\"pkg\">\(.package|h)"
241
+ + (if .location != "" then "<br>\(.location|h)" else "" end)
242
+ + (if .type != "" then "<br><span style=\"color:#868e96\">\(.type|h)</span>" else "" end)
243
+ + "</td>",
244
+ "<td class=\"pkg\">\(if .fixed == "" then "—" else (.fixed|h) end)</td>",
245
+ "<td class=\"sum\">\(.summary|h)"
246
+ + (if .applicable != "" then "<br><em>Applicability: \(.applicable|h)</em>" else "" end)
247
+ + (if .direct != "" then "<br><span style=\"color:#868e96\">via \(.direct|h)</span>" else "" end)
248
+ + (if .watch != "" then "<br><span style=\"color:#868e96\">watch: \(.watch|h)</span>" else "" end)
249
+ + "</td>",
250
+ "</tr>" ),
251
+ "</tbody></table>" )
252
+ end,
253
+
254
+ "<footer>Generated by <code>bin/xray-html-report.sh</code> from JFrog CLI simple-json output. ",
255
+ "Scans: " + ($sources | map("\(.label) [\(.status)]") | join(", ") | @html) + ".",
256
+ "</footer>",
257
+ "</body></html>"
258
+ ' "$TMP_FINDINGS" > "$OUT"
259
+
260
+ echo "HTML report written to ${OUT} (total=${TOTAL} critical=${CRITICAL} high=${HIGH} medium=${MEDIUM} low=${LOW})"
261
+
262
+ # Optional PDF, when a headless renderer happens to be on the runner.
263
+ if [[ "${XRAY_REPORT_PDF:-false}" == "true" ]]; then
264
+ PDF_OUT="${OUT%.html}.pdf"
265
+ if command -v wkhtmltopdf >/dev/null 2>&1; then
266
+ wkhtmltopdf --enable-local-file-access "$OUT" "$PDF_OUT" && echo "PDF written to ${PDF_OUT}"
267
+ else
268
+ for chrome in google-chrome chromium chromium-browser; do
269
+ if command -v "$chrome" >/dev/null 2>&1; then
270
+ "$chrome" --headless --disable-gpu --no-sandbox \
271
+ --print-to-pdf="$PDF_OUT" "file://$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")" \
272
+ && echo "PDF written to ${PDF_OUT}"
273
+ break
274
+ fi
275
+ done
276
+ fi
277
+ [[ -f "$PDF_OUT" ]] || echo "No wkhtmltopdf/chromium on this runner — skipped PDF (HTML still produced)."
278
+ fi