@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/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/validate.js +10 -0
- package/src/changeset/version.js +11 -7
- package/src/next/deploy.js +20 -2
- package/src/next/util.js +10 -0
package/bin/xray.sh
CHANGED
|
@@ -1,253 +1,236 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
#
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
149
|
+
--argjson med "$MEDIUM" \
|
|
150
|
+
--argjson low "$LOW" \
|
|
163
151
|
'{
|
|
164
152
|
channel: $channel,
|
|
165
153
|
blocks: [
|
|
166
|
-
{ type: "header",
|
|
167
|
-
|
|
168
|
-
{ type: "section",
|
|
169
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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 "✅
|
|
236
|
+
echo "✅ Posted ${POSTED} finding(s) to thread."
|