@groupby/ai-dev 0.5.16 → 0.5.17
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/package.json +1 -1
- package/teams/firstspirit-caas/mcp/jira-tools.py +2122 -0
- package/teams/firstspirit-caas/mcp/test_bamboo_artifacts.py +315 -0
- package/teams/firstspirit-caas/mcp/test_jira_links.py +192 -0
- package/teams/firstspirit-caas/mcp/test_update_jira_ticket.py +161 -0
- package/teams/firstspirit-caas/resources/mcp-setup.md +57 -0
- package/teams/firstspirit-caas/skills/chronicle/SKILL.md +105 -0
- package/teams/firstspirit-caas/skills/create-jira-ticket/SKILL.md +183 -0
- package/teams/firstspirit-caas/skills/get-jira-ticket/SKILL.md +125 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/SKILL.md +316 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/references/suppression.md +79 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/gen_suppression.py +116 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_json_report.py +183 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/parse_report.py +255 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_gen_suppression.py +171 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_json_report.py +244 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/test_parse_report.py +464 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.html +39 -0
- package/teams/firstspirit-caas/skills/handle-red-cve-plan/scripts/testdata/sample-report.json +76 -0
- package/teams/firstspirit-caas/skills/handle-weekly-cve-plans/SKILL.md +238 -0
- package/teams/firstspirit-caas/skills/retrospect/SKILL.md +94 -0
- package/teams/firstspirit-caas/skills/retrospect/extract_prompts.py +392 -0
- package/teams/firstspirit-caas/skills/review/SKILL.md +131 -0
- package/teams/firstspirit-caas/skills/review-finalize/SKILL.md +103 -0
- package/teams/firstspirit-caas/skills/specify/SKILL.md +239 -0
- package/teams/firstspirit-caas/skills/summarise-for-jira-comment/SKILL.md +162 -0
- package/teams/firstspirit-caas/skills/write-releasenotes/SKILL.md +177 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Run: python3 plugins/planetexpress/skills/handle-red-cve-plan/scripts/test_parse_json_report.py"""
|
|
2
|
+
import contextlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
_here = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
_spec = importlib.util.spec_from_file_location(
|
|
12
|
+
"parse_json_report", os.path.join(_here, "parse_json_report.py"))
|
|
13
|
+
_mod = importlib.util.module_from_spec(_spec)
|
|
14
|
+
_spec.loader.exec_module(_mod)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _dep(file_name, pkgurl, sha1, active=None, suppressed=None, included_by=None):
|
|
18
|
+
return {
|
|
19
|
+
"fileName": file_name,
|
|
20
|
+
"sha1": sha1,
|
|
21
|
+
"packages": [{"id": pkgurl}] if pkgurl else [],
|
|
22
|
+
"includedBy": included_by or [],
|
|
23
|
+
"vulnerabilities": active or [],
|
|
24
|
+
"suppressedVulnerabilities": suppressed or [],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _vuln(name, *, v3=None, v4=None, v2=None, severity=None, source="NVD",
|
|
29
|
+
description=None, references=None, software=None):
|
|
30
|
+
v = {"name": name, "source": source, "severity": severity}
|
|
31
|
+
if v3 is not None:
|
|
32
|
+
v["cvssv3"] = {"baseScore": v3, "baseSeverity": severity}
|
|
33
|
+
if v4 is not None:
|
|
34
|
+
v["cvssv4"] = {"baseScore": v4, "baseSeverity": severity}
|
|
35
|
+
if v2 is not None:
|
|
36
|
+
v["cvssv2"] = {"score": v2, "severity": severity}
|
|
37
|
+
if description is not None:
|
|
38
|
+
v["description"] = description
|
|
39
|
+
if references is not None:
|
|
40
|
+
v["references"] = references
|
|
41
|
+
if software is not None:
|
|
42
|
+
v["vulnerableSoftware"] = software
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _report(deps):
|
|
47
|
+
return {"reportSchema": "1.1", "dependencies": deps}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestParse(unittest.TestCase):
|
|
51
|
+
def test_keeps_active_high_cve(self):
|
|
52
|
+
rpt = _report([_dep("httpcore-4.4.16.jar",
|
|
53
|
+
"pkg:maven/org.apache.httpcomponents/httpcore@4.4.16",
|
|
54
|
+
"51cf043c87253c9f58b539c9f7e44c8894223850",
|
|
55
|
+
active=[_vuln("CVE-2026-54399", v3=7.5, severity="HIGH")])])
|
|
56
|
+
findings = _mod.parse(rpt)["findings"]
|
|
57
|
+
self.assertEqual({f["cveId"] for f in findings}, {"CVE-2026-54399"})
|
|
58
|
+
|
|
59
|
+
def test_finding_fields(self):
|
|
60
|
+
rpt = _report([_dep(
|
|
61
|
+
"httpcore-4.4.16.jar",
|
|
62
|
+
"pkg:maven/org.apache.httpcomponents/httpcore@4.4.16",
|
|
63
|
+
"51cf043c87253c9f58b539c9f7e44c8894223850",
|
|
64
|
+
active=[_vuln("CVE-2026-54399", v3=7.5, severity="HIGH",
|
|
65
|
+
description="Uncontrolled Resource Consumption in 5.4.2 and earlier",
|
|
66
|
+
references=[{"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54399"},
|
|
67
|
+
{"url": "https://lists.apache.org/thread/abc123"}],
|
|
68
|
+
software=[{"software": {
|
|
69
|
+
"id": "cpe:2.3:a:apache:httpcomponents_core:*:*:*:*:*:*:*:*",
|
|
70
|
+
"versionEndIncluding": "5.4.2"}}])])])
|
|
71
|
+
f = _mod.parse(rpt)["findings"][0]
|
|
72
|
+
self.assertEqual(f["cveId"], "CVE-2026-54399")
|
|
73
|
+
self.assertEqual(f["score"], 7.5)
|
|
74
|
+
self.assertEqual(f["severity"], "HIGH")
|
|
75
|
+
self.assertEqual(f["dependency"]["fileName"], "httpcore-4.4.16.jar")
|
|
76
|
+
self.assertEqual(f["dependency"]["packageUrl"],
|
|
77
|
+
"pkg:maven/org.apache.httpcomponents/httpcore@4.4.16")
|
|
78
|
+
self.assertEqual(f["dependency"]["sha1"],
|
|
79
|
+
"51cf043c87253c9f58b539c9f7e44c8894223850")
|
|
80
|
+
self.assertIn("Uncontrolled Resource Consumption", f["description"])
|
|
81
|
+
self.assertIn("https://nvd.nist.gov/vuln/detail/CVE-2026-54399", f["references"])
|
|
82
|
+
self.assertTrue(any("5.4.2" in a for a in f["affectedVersions"]))
|
|
83
|
+
|
|
84
|
+
def test_suppressed_vulnerability_is_dropped(self):
|
|
85
|
+
rpt = _report([_dep("kotlin.jar", "pkg:maven/org.jetbrains.kotlin/kotlin@2.4.0", "aa",
|
|
86
|
+
suppressed=[_vuln("CVE-2026-53914", v3=9.8, severity="CRITICAL")])])
|
|
87
|
+
result = _mod.parse(rpt)
|
|
88
|
+
self.assertEqual(result["findings"], [])
|
|
89
|
+
self.assertEqual(result["unparsed"], [])
|
|
90
|
+
|
|
91
|
+
def test_below_high_excluded(self):
|
|
92
|
+
rpt = _report([_dep("a.jar", "pkg:maven/g/a@1", "aa",
|
|
93
|
+
active=[_vuln("CVE-2026-0001", v3=6.5, severity="MEDIUM")])])
|
|
94
|
+
self.assertEqual(_mod.parse(rpt)["findings"], [])
|
|
95
|
+
|
|
96
|
+
def test_cvss_exactly_seven_is_kept(self):
|
|
97
|
+
rpt = _report([_dep("a.jar", "pkg:maven/g/a@1", "aa",
|
|
98
|
+
active=[_vuln("CVE-2026-0007", v3=7.0, severity="HIGH")])])
|
|
99
|
+
f = _mod.parse(rpt)["findings"][0]
|
|
100
|
+
self.assertEqual(f["cveId"], "CVE-2026-0007")
|
|
101
|
+
self.assertEqual(f["score"], 7.0)
|
|
102
|
+
|
|
103
|
+
def test_score_is_max_across_cvss_versions(self):
|
|
104
|
+
"""A vuln carrying several CVSS versions reddens on the worst of them;
|
|
105
|
+
take the max so a >=7 score in any version is never gated below."""
|
|
106
|
+
rpt = _report([_dep("a.jar", "pkg:maven/g/a@1", "aa",
|
|
107
|
+
active=[_vuln("CVE-2026-0009", v2=5.0, v3=7.8, v4=4.8,
|
|
108
|
+
severity="HIGH")])])
|
|
109
|
+
f = _mod.parse(rpt)["findings"][0]
|
|
110
|
+
self.assertEqual(f["score"], 7.8)
|
|
111
|
+
self.assertEqual(f["severity"], "HIGH")
|
|
112
|
+
|
|
113
|
+
def test_cvssv4_float_precision_is_rounded(self):
|
|
114
|
+
rpt = _report([_dep("a.jar", "pkg:maven/g/a@1", "aa",
|
|
115
|
+
active=[_vuln("CVE-2026-0010", v4=7.800000190734863,
|
|
116
|
+
severity="HIGH")])])
|
|
117
|
+
self.assertEqual(_mod.parse(rpt)["findings"][0]["score"], 7.8)
|
|
118
|
+
|
|
119
|
+
def test_band_only_high_cve_becomes_finding_with_null_score(self):
|
|
120
|
+
"""A CVE with no numeric CVSS but a HIGH severity band is at/above the
|
|
121
|
+
gate -- a finding with score None, severity from the band."""
|
|
122
|
+
rpt = _report([_dep("a.jar", "pkg:maven/g/a@1", "aa",
|
|
123
|
+
active=[_vuln("CVE-2026-0002", severity="HIGH")])])
|
|
124
|
+
f = _mod.parse(rpt)["findings"][0]
|
|
125
|
+
self.assertEqual(f["cveId"], "CVE-2026-0002")
|
|
126
|
+
self.assertIsNone(f["score"])
|
|
127
|
+
self.assertEqual(f["severity"], "HIGH")
|
|
128
|
+
|
|
129
|
+
def test_band_only_medium_cve_excluded(self):
|
|
130
|
+
rpt = _report([_dep("a.jar", "pkg:maven/g/a@1", "aa",
|
|
131
|
+
active=[_vuln("CVE-2026-0003", severity="medium")])])
|
|
132
|
+
result = _mod.parse(rpt)
|
|
133
|
+
self.assertEqual(result["findings"], [])
|
|
134
|
+
self.assertEqual(result["unparsed"], [])
|
|
135
|
+
|
|
136
|
+
def test_non_cve_high_goes_to_unparsed(self):
|
|
137
|
+
rpt = _report([_dep("foo.js", "pkg:javascript/foo@1.0", "aa",
|
|
138
|
+
active=[_vuln("GHSA-dddd-eeee-ffff", severity="high",
|
|
139
|
+
source="RETIREJS")])])
|
|
140
|
+
result = _mod.parse(rpt)
|
|
141
|
+
self.assertEqual(result["findings"], [])
|
|
142
|
+
u = next(x for x in result["unparsed"] if x["cveId"] == "GHSA-dddd-eeee-ffff")
|
|
143
|
+
self.assertEqual(u["fileName"], "foo.js")
|
|
144
|
+
self.assertIn("non-CVE", u["reason"])
|
|
145
|
+
|
|
146
|
+
def test_non_cve_medium_excluded(self):
|
|
147
|
+
rpt = _report([_dep("foo.js", "pkg:javascript/foo@1.0", "aa",
|
|
148
|
+
active=[_vuln("GHSA-aaaa-bbbb-cccc", severity="medium",
|
|
149
|
+
source="RETIREJS")])])
|
|
150
|
+
result = _mod.parse(rpt)
|
|
151
|
+
self.assertEqual(result["findings"], [])
|
|
152
|
+
self.assertEqual(result["unparsed"], [])
|
|
153
|
+
|
|
154
|
+
def test_no_score_and_no_band_goes_to_unparsed(self):
|
|
155
|
+
rpt = _report([_dep("mystery.jar", "pkg:maven/g/mystery@1", "99",
|
|
156
|
+
active=[_vuln("CVE-2026-9999", severity=None)])])
|
|
157
|
+
result = _mod.parse(rpt)
|
|
158
|
+
self.assertNotIn("CVE-2026-9999", {f["cveId"] for f in result["findings"]})
|
|
159
|
+
u = next(x for x in result["unparsed"] if x["cveId"] == "CVE-2026-9999")
|
|
160
|
+
self.assertEqual(u["fileName"], "mystery.jar")
|
|
161
|
+
self.assertEqual(u["packageUrl"], "pkg:maven/g/mystery@1")
|
|
162
|
+
self.assertEqual(u["sha1"], "99")
|
|
163
|
+
|
|
164
|
+
def test_unparsed_deduplicated_by_id_and_file(self):
|
|
165
|
+
v = _vuln("GHSA-dddd-eeee-ffff", severity="high", source="RETIREJS")
|
|
166
|
+
rpt = _report([_dep("foo.js", "pkg:javascript/foo@1.0", "aa", active=[v, dict(v)])])
|
|
167
|
+
result = _mod.parse(rpt)
|
|
168
|
+
matches = [u for u in result["unparsed"] if u["cveId"] == "GHSA-dddd-eeee-ffff"]
|
|
169
|
+
self.assertEqual(len(matches), 1)
|
|
170
|
+
|
|
171
|
+
def test_same_cve_on_two_files_yields_two_findings(self):
|
|
172
|
+
rpt = _report([
|
|
173
|
+
_dep("httpcore-4.4.16.jar", "pkg:maven/g/httpcore@4.4.16", "a1",
|
|
174
|
+
active=[_vuln("CVE-2026-54399", v3=7.5, severity="HIGH")]),
|
|
175
|
+
_dep("httpcore5-5.1.jar", "pkg:maven/g/httpcore5@5.1", "b2",
|
|
176
|
+
active=[_vuln("CVE-2026-54399", v3=7.5, severity="HIGH")]),
|
|
177
|
+
])
|
|
178
|
+
findings = _mod.parse(rpt)["findings"]
|
|
179
|
+
files = sorted(f["dependency"]["fileName"] for f in findings)
|
|
180
|
+
self.assertEqual(files, ["httpcore-4.4.16.jar", "httpcore5-5.1.jar"])
|
|
181
|
+
|
|
182
|
+
def test_included_by_captured_when_present(self):
|
|
183
|
+
rpt = _report([_dep("solrj.jar", "pkg:maven/org.apache.solr/solr-solrj@9.9.0", "aa",
|
|
184
|
+
included_by=[{"reference": "pkg:maven/root/app@1.0"}],
|
|
185
|
+
active=[_vuln("CVE-2026-0011", v3=8.1, severity="HIGH")])])
|
|
186
|
+
f = _mod.parse(rpt)["findings"][0]
|
|
187
|
+
self.assertEqual(f["includedBy"], ["pkg:maven/root/app@1.0"])
|
|
188
|
+
|
|
189
|
+
def test_missing_packages_yields_null_package_url(self):
|
|
190
|
+
rpt = _report([_dep("x.jar", None, "aa",
|
|
191
|
+
active=[_vuln("CVE-2026-0012", v3=7.5, severity="HIGH")])])
|
|
192
|
+
f = _mod.parse(rpt)["findings"][0]
|
|
193
|
+
self.assertIsNone(f["dependency"]["packageUrl"])
|
|
194
|
+
|
|
195
|
+
def test_fixture_report(self):
|
|
196
|
+
with open(os.path.join(_here, "testdata", "sample-report.json"), encoding="utf-8") as fh:
|
|
197
|
+
rpt = json.load(fh)
|
|
198
|
+
result = _mod.parse(rpt)
|
|
199
|
+
self.assertEqual({f["cveId"] for f in result["findings"]}, {"CVE-2026-54399"})
|
|
200
|
+
self.assertNotIn("CVE-2026-53914", {f["cveId"] for f in result["findings"]})
|
|
201
|
+
self.assertIn("GHSA-abcd-1234-wxyz", {u["cveId"] for u in result["unparsed"]})
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestMain(unittest.TestCase):
|
|
205
|
+
def setUp(self):
|
|
206
|
+
self.sample = os.path.join(_here, "testdata", "sample-report.json")
|
|
207
|
+
|
|
208
|
+
def test_out_writes_valid_json_and_exits_zero(self):
|
|
209
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
210
|
+
out_path = os.path.join(tmpdir, "out.json")
|
|
211
|
+
rc = _mod.main([self.sample, "--out", out_path])
|
|
212
|
+
self.assertEqual(rc, 0)
|
|
213
|
+
with open(out_path, encoding="utf-8") as fh:
|
|
214
|
+
data = json.load(fh)
|
|
215
|
+
self.assertEqual({f["cveId"] for f in data["findings"]}, {"CVE-2026-54399"})
|
|
216
|
+
|
|
217
|
+
def test_missing_input_file_exits_nonzero_with_stderr_message(self):
|
|
218
|
+
stderr = io.StringIO()
|
|
219
|
+
with contextlib.redirect_stderr(stderr):
|
|
220
|
+
rc = _mod.main(["/nonexistent/path/does-not-exist.json"])
|
|
221
|
+
self.assertNotEqual(rc, 0)
|
|
222
|
+
self.assertIn("parse_json_report.py:", stderr.getvalue())
|
|
223
|
+
|
|
224
|
+
def test_malformed_json_exits_nonzero_with_stderr_message(self):
|
|
225
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
226
|
+
bad = os.path.join(tmpdir, "bad.json")
|
|
227
|
+
with open(bad, "w", encoding="utf-8") as fh:
|
|
228
|
+
fh.write("{not valid json")
|
|
229
|
+
stderr = io.StringIO()
|
|
230
|
+
with contextlib.redirect_stderr(stderr):
|
|
231
|
+
rc = _mod.main([bad])
|
|
232
|
+
self.assertNotEqual(rc, 0)
|
|
233
|
+
self.assertIn("parse_json_report.py:", stderr.getvalue())
|
|
234
|
+
|
|
235
|
+
def test_unwritable_out_path_exits_nonzero_with_stderr_message(self):
|
|
236
|
+
stderr = io.StringIO()
|
|
237
|
+
with contextlib.redirect_stderr(stderr):
|
|
238
|
+
rc = _mod.main([self.sample, "--out", "/nonexistent-dir/out.json"])
|
|
239
|
+
self.assertNotEqual(rc, 0)
|
|
240
|
+
self.assertIn("cannot write output", stderr.getvalue())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
unittest.main()
|