@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.
- package/bin/tenable-scan.sh +224 -0
- package/bin/xray-bitbucket-insights.sh +287 -0
- package/bin/xray-html-report.sh +278 -0
- package/bin/xray.sh +204 -221
- package/bitbucket-pipelines.yml +291 -0
- package/package.json +1 -1
- package/src/changeset/cli.js +59 -33
- package/src/changeset/convert.js +7 -0
- package/src/changeset/version.js +11 -7
- package/src/next/deploy.js +20 -2
|
@@ -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> · branch <code>\($branch|h)</code> · commit <code>\($commit[0:12]|h)</code><br>",
|
|
202
|
+
"Generated \($generated|h)",
|
|
203
|
+
(if $buildurl != "" then " · <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
|