@docupace/pipeline 1.0.121 → 1.0.122-connect.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/bin/xray.sh CHANGED
@@ -1,253 +1,236 @@
1
1
  #!/usr/bin/env bash
2
- BASENAME="${BITBUCKET_REPO_SLUG}"
3
- SLACK_API="https://slack.com/api/chat.postMessage"
4
- INPUT_TABLE="xray.txt"
5
- INPUT_JSON="xray.json"
6
-
7
- # ------------------------------------------------
8
- # 1️⃣ Parse table to JSON (handle multi-line wrapping)
9
- # ------------------------------------------------
10
-
11
- awk '
12
- BEGIN {
13
- print "["
14
- in_vuln = 0
15
- }
16
-
17
- # Skip separator and header lines
18
- /^[┌├└]/ { next }
19
- /CVE.*SEVERITY.*DIRECT.*DEPENDENCY/ { next }
20
-
21
- # Process data rows
22
- /^│/ {
23
- gsub(/^│ /, "")
24
- gsub(/ *│ *$/, "")
25
-
26
- n = split($0, fields, / *│ */)
27
-
28
- # New vulnerability (has CVE in first column)
29
- if (n >= 8 && fields[1] ~ /^CVE-/) {
30
- # Output previous entry if exists
31
- if (in_vuln) {
32
- # Parse fixed versions
33
- fixed_versions = "[]"
34
- if (saved_fixed_raw != "") {
35
- gsub(/\] *\[/, "][", saved_fixed_raw)
36
- gsub(/\[/, "\"", saved_fixed_raw)
37
- gsub(/\]/, "\"", saved_fixed_raw)
38
- gsub(/" *"/, "\", \"", saved_fixed_raw)
39
- gsub(/""/, "\", \"", saved_fixed_raw)
40
- fixed_versions = "[" saved_fixed_raw "]"
41
- }
42
-
43
- component_key = saved_type "://" saved_affected
44
- direct_component = saved_type "://" saved_direct
45
-
46
- print " {"
47
- print " \"vulnerabilities\": [{"
48
- print " \"severity\": \"" saved_severity "\","
49
- print " \"cves\": [{\"cve\": \"" saved_cve "\"}],"
50
- print " \"components\": {"
51
- print " \"" component_key "\": {"
52
- print " \"impact_paths\": [[null, {\"component_id\": \"" direct_component "\"}]],"
53
- print " \"affected_version\": \"" saved_affected_ver "\","
54
- print " \"fixed_versions\": " fixed_versions
55
- print " }"
56
- print " }"
57
- print " }]"
58
- print " },"
59
- }
2
+ #
3
+ # Posts Xray findings to Slack: a parent summary message and one thread reply
4
+ # per finding (High and above, sorted Critical→High→Medium→Low).
5
+ #
6
+ # Reads simple-json files listed in /tmp/xray-report-args (written by the Xray
7
+ # scan step), so it no longer depends on the fragile awk table parser.
8
+ #
9
+ # Required:
10
+ # XRAY_SLACK_TOKEN — Slack bot token (xoxb-...)
11
+ # XRAY_SLACK_CHANNEL — channel ID or name, e.g. #security-alerts
12
+ # Optional:
13
+ # XRAY_SLACK_MIN_SEVERITY — lowest severity to thread (default: High)
14
+ # Critical | High | Medium | Low
15
+ # XRAY_SLACK_NOTIFY_CLEAN — set to "true" to post even when total=0
16
+ # BITBUCKET_REPO_SLUG, BITBUCKET_BUILD_NUMBER, BITBUCKET_BRANCH
17
+
18
+ set -uo pipefail
60
19
 
61
- in_vuln = 1
62
-
63
- # Store new vulnerability
64
- saved_cve = fields[1]
65
- saved_severity = fields[2]
66
- saved_direct = fields[3]
67
-
68
- saved_affected = fields[5]
69
- saved_affected_ver = fields[6]
70
- saved_fixed_raw = fields[7]
71
- saved_type = tolower(fields[8])
72
- }
73
- # Continuation line - append to previous field
74
- else if (in_vuln && fields[1] == "" && fields[2] == "") {
75
- if (fields[3] != "") {
76
- saved_direct = saved_direct fields[3]
77
- }
78
- if (fields[5] != "") {
79
- saved_affected = saved_affected fields[5]
80
- }
81
- if (fields[7] != "") {
82
- saved_fixed_raw = saved_fixed_raw " " fields[7]
83
- }
84
- }
20
+ SLACK_API="https://slack.com/api/chat.postMessage"
21
+ BASENAME="${BITBUCKET_REPO_SLUG:-unknown}"
22
+ MIN_SEV="${XRAY_SLACK_MIN_SEVERITY:-High}"
23
+ REPORT_ARGS_FILE="${XRAY_REPORT_ARGS_FILE:-/tmp/xray-report-args}"
24
+
25
+ # Map severity to a sort rank (lower = shown first).
26
+ rank_of() {
27
+ case "$1" in
28
+ Critical) echo 0 ;;
29
+ High) echo 1 ;;
30
+ Medium) echo 2 ;;
31
+ Low) echo 3 ;;
32
+ *) echo 9 ;;
33
+ esac
85
34
  }
35
+ MIN_RANK="$(rank_of "$MIN_SEV")"
86
36
 
87
- END {
88
- if (in_vuln) {
89
- # Output the last entry
90
- fixed_versions = "[]"
91
- if (saved_fixed_raw != "") {
92
- gsub(/\] *\[/, "][", saved_fixed_raw)
93
- gsub(/\[/, "\"", saved_fixed_raw)
94
- gsub(/\]/, "\"", saved_fixed_raw)
95
- gsub(/" *"/, "\", \"", saved_fixed_raw)
96
- gsub(/""/, "\", \"", saved_fixed_raw)
97
- fixed_versions = "[" saved_fixed_raw "]"
98
- }
37
+ # ---------------------------------------------------------------------------
38
+ # Collect findings from every simple-json file listed in the report-args file.
39
+ # The step writes one "label:file" entry per completed scan.
40
+ # ---------------------------------------------------------------------------
41
+ TMP_JSONL="$(mktemp)"
42
+ trap 'rm -f "$TMP_JSONL"' EXIT
99
43
 
100
- component_key = saved_type "://" saved_affected
101
- direct_component = saved_type "://" saved_direct
102
-
103
- print " {"
104
- print " \"vulnerabilities\": [{"
105
- print " \"severity\": \"" saved_severity "\","
106
- print " \"cves\": [{\"cve\": \"" saved_cve "\"}],"
107
- print " \"components\": {"
108
- print " \"" component_key "\": {"
109
- print " \"impact_paths\": [[null, {\"component_id\": \"" direct_component "\"}]],"
110
- print " \"affected_version\": \"" saved_affected_ver "\","
111
- print " \"fixed_versions\": " fixed_versions
112
- print " }"
113
- print " }"
114
- print " }]"
115
- print " }"
116
- }
117
- print "]"
118
- }
119
- ' "$INPUT_TABLE" > "$INPUT_JSON"
120
-
121
- # Validate JSON
122
- if ! jq empty "$INPUT_JSON" 2>/dev/null; then
123
- echo "❌ Failed to create valid JSON"
124
- cat "$INPUT_JSON"
125
- exit 1
44
+ if [[ ! -s "$REPORT_ARGS_FILE" ]]; then
45
+ echo "No scan results file at ${REPORT_ARGS_FILE} — skipping Slack notification."
46
+ exit 0
126
47
  fi
127
48
 
128
- echo "✅ JSON created: $INPUT_JSON"
129
-
130
- # ------------------------------------------------
131
- # 2️⃣ Severity counts
132
- # ------------------------------------------------
133
- read CRITICAL HIGH MEDIUM LOW <<< $(
134
- jq -r '
135
- .[]
136
- | select(.vulnerabilities != null)
137
- | .vulnerabilities[]
138
- | .severity
139
- ' "$INPUT_JSON" | awk '
140
- { c[$1]++ }
141
- END {
142
- print c["Critical"]+0, c["High"]+0, c["Medium"]+0, c["Low"]+0
143
- }'
144
- )
145
-
146
- echo "📊 Counts: Critical=$CRITICAL, High=$HIGH, Medium=$MEDIUM, Low=$LOW"
147
-
49
+ while IFS= read -r arg; do
50
+ [[ -z "$arg" ]] && continue
51
+ scan_label="${arg%%:*}"
52
+ json_file="${arg#*:}"
53
+ if [[ ! -s "$json_file" ]] || ! jq empty "$json_file" >/dev/null 2>&1; then
54
+ echo "Skipping ${scan_label} (${json_file}): not produced or empty."
55
+ continue
56
+ fi
57
+
58
+ # Emit one JSONL record per finding. SCA rows have a package + CVEs; source-
59
+ # code rows (secrets, SAST, IaC) have a file path.
60
+ jq -c --arg scan "$scan_label" '
61
+ def sev: (.severity // "Unknown");
62
+
63
+ def sca_rows(rows; kind):
64
+ (rows // [])[] | {
65
+ scan: $scan,
66
+ kind: kind,
67
+ severity: sev,
68
+ rank: (sev | if . == "Critical" then 0
69
+ elif . == "High" then 1
70
+ elif . == "Medium" then 2
71
+ elif . == "Low" then 3
72
+ else 9 end),
73
+ cve: (((.cves // []) | map(.cve // empty) | first) // (.issueId // "N/A")),
74
+ cves_all: ((.cves // []) | map(.cve // empty)),
75
+ pkg: ((.impactedPackageName // "N/A") + " " + (.impactedPackageVersion // "")),
76
+ fixed: ((.fixedVersions // []) | join(", ")),
77
+ directs: ((.components // []) | map(.name // empty) | unique | join(", ")),
78
+ path: "",
79
+ line: 0
80
+ };
81
+
82
+ def code_rows(rows; kind):
83
+ (rows // [])[] | {
84
+ scan: $scan,
85
+ kind: kind,
86
+ severity: sev,
87
+ rank: (sev | if . == "Critical" then 0
88
+ elif . == "High" then 1
89
+ elif . == "Medium" then 2
90
+ elif . == "Low" then 3
91
+ else 9 end),
92
+ cve: (.ruleId // "N/A"),
93
+ cves_all: [],
94
+ pkg: (.finding // .scannerShortDescription // "N/A"),
95
+ fixed: "",
96
+ directs: "",
97
+ path: (.file // ""),
98
+ line: (.startLine // 0)
99
+ };
100
+
101
+ [ sca_rows(.vulnerabilities; "Vulnerability")
102
+ , sca_rows(.securityViolations; "Security violation")
103
+ , sca_rows(.licensesViolations; "License violation")
104
+ , sca_rows(.operationalRiskViolations;"Operational risk")
105
+ , code_rows(.secrets; "Secret")
106
+ , code_rows(.secretsViolations; "Secret violation")
107
+ , code_rows(.sast; "SAST")
108
+ , code_rows(.sastViolations; "SAST violation")
109
+ , code_rows(.iac; "IaC")
110
+ , code_rows(.iacViolations; "IaC violation")
111
+ , code_rows(.maliciousCode; "Malicious code")
112
+ ] | flatten | .[]
113
+ ' "$json_file" 2>/dev/null >> "$TMP_JSONL"
114
+ done < "$REPORT_ARGS_FILE"
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Severity counts (all findings, not just the threaded subset).
118
+ # ---------------------------------------------------------------------------
119
+ count_sev() { jq -s --arg s "$1" '[.[]|select(.severity==$s)]|length' "$TMP_JSONL"; }
120
+ CRITICAL=$(count_sev Critical)
121
+ HIGH=$(count_sev High)
122
+ MEDIUM=$(count_sev Medium)
123
+ LOW=$(count_sev Low)
148
124
  TOTAL=$((CRITICAL + HIGH + MEDIUM + LOW))
149
- if [[ "$TOTAL" -eq 0 ]]; then
150
- echo "✅ No vulnerabilities found, skipping Slack notification"
125
+
126
+ echo "📊 Counts: Critical=${CRITICAL}, High=${HIGH}, Medium=${MEDIUM}, Low=${LOW}, Total=${TOTAL}"
127
+
128
+ if [[ "$TOTAL" -eq 0 && "${XRAY_SLACK_NOTIFY_CLEAN:-false}" != "true" ]]; then
129
+ echo "✅ No vulnerabilities found, skipping Slack notification."
151
130
  exit 0
152
131
  fi
153
132
 
154
- # ------------------------------------------------
155
- # 3️⃣ Parent Slack message
156
- # ------------------------------------------------
157
- PARENT_PAYLOAD=$(jq -cn \
158
- --arg channel "$XRAY_SLACK_CHANNEL" \
159
- --arg project "$BASENAME" \
160
- --argjson critical "$CRITICAL" \
133
+ # ---------------------------------------------------------------------------
134
+ # Parent summary message.
135
+ # ---------------------------------------------------------------------------
136
+ BUILD_LINK=""
137
+ if [[ -n "${BITBUCKET_WORKSPACE:-}" && -n "${BITBUCKET_REPO_SLUG:-}" && -n "${BITBUCKET_BUILD_NUMBER:-}" ]]; then
138
+ BUILD_LINK="https://bitbucket.org/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPO_SLUG}/pipelines/results/${BITBUCKET_BUILD_NUMBER}"
139
+ fi
140
+ BRANCH="${BITBUCKET_BRANCH:-}"
141
+
142
+ PARENT_PAYLOAD="$(jq -cn \
143
+ --arg channel "$XRAY_SLACK_CHANNEL" \
144
+ --arg project "$BASENAME" \
145
+ --arg branch "$BRANCH" \
146
+ --arg link "$BUILD_LINK" \
147
+ --argjson crit "$CRITICAL" \
161
148
  --argjson high "$HIGH" \
162
- --argjson medium "$MEDIUM" \
149
+ --argjson med "$MEDIUM" \
150
+ --argjson low "$LOW" \
163
151
  '{
164
152
  channel: $channel,
165
153
  blocks: [
166
- { type: "header", text: { type: "plain_text", text: "🚨 Dependency Vulnerability Report" } },
167
- { type: "section", text: { type: "mrkdwn", text: ("*Project:* " + $project) } },
168
- { type: "section", text: { type: "mrkdwn", text: ("🔴 *Critical:* \($critical) 🔥 *High:* \($high) 🎃 *Medium:* \($medium)") } },
169
- { type: "context", elements: [{ type: "mrkdwn", text: "Detailed findings are posted in the thread below 👇" }] },
154
+ { type: "header",
155
+ text: { type: "plain_text", text: "🚨 Xray Security Report" } },
156
+ { type: "section",
157
+ text: { type: "mrkdwn",
158
+ text: ("*Project:* " + $project
159
+ + (if $branch != "" then (" | *Branch:* " + $branch) else "" end)) } },
160
+ { type: "section",
161
+ text: { type: "mrkdwn",
162
+ text: ("🔴 *Critical:* \($crit) 🔥 *High:* \($high) 🎃 *Medium:* \($med) 👻 *Low:* \($low)") } },
163
+ { type: "context",
164
+ elements: [{ type: "mrkdwn",
165
+ text: (if $link != ""
166
+ then ("Detailed findings are in the thread below 👇 • <" + $link + "|Pipeline run>")
167
+ else "Detailed findings are in the thread below 👇"
168
+ end) }] },
170
169
  { type: "divider" }
171
170
  ]
172
- }')
171
+ }')"
173
172
 
174
- PARENT_RESPONSE=$(printf '%s' "$PARENT_PAYLOAD" | curl -s -X POST "$SLACK_API" \
173
+ PARENT_RESPONSE="$(printf '%s' "$PARENT_PAYLOAD" | curl -s -X POST "$SLACK_API" \
175
174
  -H "Authorization: Bearer $XRAY_SLACK_TOKEN" \
176
175
  -H "Content-Type: application/json" \
177
- --data @-)
176
+ --data @-)"
178
177
 
179
- THREAD_TS=$(echo "$PARENT_RESPONSE" | jq -r '.ts')
178
+ THREAD_TS="$(printf '%s' "$PARENT_RESPONSE" | jq -r '.ts')"
180
179
 
181
180
  if [[ -z "$THREAD_TS" || "$THREAD_TS" == "null" ]]; then
182
- echo "❌ Failed to create parent message"
183
- echo "$PARENT_RESPONSE"
181
+ echo "❌ Failed to create parent Slack message:"
182
+ printf '%s\n' "$PARENT_RESPONSE" | head -5
184
183
  exit 1
185
184
  fi
186
185
 
187
- echo "✅ Parent message created. thread_ts=$THREAD_TS"
188
-
189
- # ------------------------------------------------
190
- # 4️⃣ Thread entries (sorted, CVE links)
191
- # ------------------------------------------------
192
- jq -r '
193
- def rank:
194
- if . == "Critical" then 0
195
- elif . == "High" then 1
196
- elif . == "Medium" then 2
197
- elif . == "Low" then 3
198
- else 9 end;
199
-
200
- [
201
- .[]
202
- | select(.vulnerabilities != null)
203
- | .vulnerabilities[]
204
- | . as $v
205
- | ($v.components | to_entries[]) as $comp
206
- | ($v.cves // [{cve:"N/A"}])[]
207
- | {
208
- sev: $v.severity,
209
- r: ($v.severity | rank),
210
- text: "*<https://nvd.nist.gov/vuln/detail/\(.cve)|\(.cve)>*\n*Impacted:* `\(
211
- $comp.key | sub("^[a-z]+://"; "")
212
- )` v\(
213
- $comp.value.affected_version // "N/A"
214
- )\n*Fixed in:* \(
215
- ($comp.value.fixed_versions // [])
216
- | join(", ")
217
- | if . == "" then "N/A" else . end
218
- )\n*Direct dependencies:*\n• \(
219
- $comp.value.impact_paths[0][1].component_id
220
- | sub("^[a-z]+://"; "")
221
- )"
222
- }
223
- ]
224
- | sort_by(.r)
225
- | .[]
226
- | [.sev, .text]
227
- | @tsv
228
- ' "$INPUT_JSON" |
229
- while IFS=$'\t' read -r SEV TEXT; do
230
-
231
- EMOJI="👻"
232
- [[ "$SEV" == "Critical" ]] && EMOJI="🔴"
233
- [[ "$SEV" == "High" ]] && EMOJI="🔥"
234
- [[ "$SEV" == "Medium" ]] && EMOJI="🎃"
235
-
236
- jq -cn \
237
- --arg channel "$XRAY_SLACK_CHANNEL" \
238
- --arg ts "$THREAD_TS" \
239
- --arg body "$EMOJI *$SEV*\n$TEXT" \
240
- '{
241
- channel: $channel,
186
+ echo "✅ Parent message created. thread_ts=${THREAD_TS}"
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Thread entries: one per finding at or above MIN_SEV, sorted by severity.
190
+ # ---------------------------------------------------------------------------
191
+ POSTED=0
192
+ while IFS= read -r finding; do
193
+ [[ -z "$finding" ]] && continue
194
+
195
+ rank="$(printf '%s' "$finding" | jq -r '.rank')"
196
+ [[ "$rank" -gt "$MIN_RANK" ]] && continue
197
+
198
+ payload="$(printf '%s' "$finding" | jq -c \
199
+ --arg channel "$XRAY_SLACK_CHANNEL" \
200
+ --arg ts "$THREAD_TS" '
201
+ (if .severity == "Critical" then "🔴"
202
+ elif .severity == "High" then "🔥"
203
+ elif .severity == "Medium" then "🎃"
204
+ else "👻" end) as $emoji |
205
+ # Link CVE IDs to NVD; fall back to plain text for rule IDs and non-CVE ids.
206
+ (if (.cves_all | length) > 0
207
+ then (.cves_all | map("<https://nvd.nist.gov/vuln/detail/\(.)|\(.)>") | join(", "))
208
+ else (.cve | if test("^CVE-") then "<https://nvd.nist.gov/vuln/detail/\(.)|\(.)>" else . end)
209
+ end) as $id_text |
210
+ ($emoji + " *" + .severity + "* — " + .scan + " (" + .kind + ")"
211
+ + "\n*ID:* " + $id_text
212
+ + "\n*Component:* `" + .pkg + "`"
213
+ + (if .fixed != "" then "\n*Fixed in:* " + .fixed else "" end)
214
+ + (if .directs != "" then "\n*Via:* " + .directs else "" end)
215
+ + (if .path != "" then "\n*File:* `" + .path + (if .line > 0 then ":" + (.line|tostring) else "" end) + "`" else "" end)
216
+ ) as $body |
217
+ {
218
+ channel: $channel,
242
219
  thread_ts: $ts,
243
220
  blocks: [{ type: "section", text: { type: "mrkdwn", text: $body } }]
244
- }' | curl -s -X POST "$SLACK_API" \
245
- -H "Authorization: Bearer $XRAY_SLACK_TOKEN" \
246
- -H "Content-Type: application/json" \
247
- --data @- > /dev/null
221
+ }
222
+ ')"
223
+
224
+ [[ -z "$payload" ]] && continue
225
+
226
+ printf '%s' "$payload" | curl -s -X POST "$SLACK_API" \
227
+ -H "Authorization: Bearer $XRAY_SLACK_TOKEN" \
228
+ -H "Content-Type: application/json" \
229
+ --data @- > /dev/null
248
230
 
231
+ POSTED=$((POSTED + 1))
249
232
  sleep 0.1
250
233
 
251
- done
234
+ done < <(jq -s -c 'sort_by(.rank) | .[]' "$TMP_JSONL")
252
235
 
253
- echo "✅ All vulnerabilities posted to thread"
236
+ echo "✅ Posted ${POSTED} finding(s) to thread."