github-manage-security-alerts-skill 1.0.0 → 1.0.2

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.
@@ -1,318 +1,310 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from typing import Any
5
-
6
- from github_security_common import expect_dict
7
-
8
-
9
- def render_text(command: str, payload: Any) -> str:
10
- """Render command output as human-readable text."""
11
-
12
- if command == "summary":
13
- return render_summary(payload)
14
- if command == "repo-security-overview":
15
- return render_repo_security_overview(payload)
16
- if command == "bulk-update-alerts":
17
- return render_json_like(payload)
18
- if command == "list-code-scanning":
19
- return render_code_scanning_list(payload)
20
- if command == "show-code-scanning":
21
- return render_json_like(payload)
22
- if command == "update-code-scanning":
23
- return render_update_result(payload)
24
- if command == "list-dependabot":
25
- return render_dependabot_list(payload)
26
- if command == "show-dependabot":
27
- return render_json_like(payload)
28
- if command == "update-dependabot":
29
- return render_update_result(payload)
30
- if command == "list-malware":
31
- return render_dependabot_list(
32
- payload, heading="Dependabot malware alerts"
33
- )
34
- if command == "show-malware":
35
- return render_json_like(payload)
36
- if command == "update-malware":
37
- return render_update_result(payload)
38
- if command == "list-secret-scanning":
39
- return render_secret_scanning_list(payload)
40
- if command == "show-secret-scanning":
41
- return render_json_like(payload)
42
- if command == "update-secret-scanning":
43
- return render_update_result(payload)
44
- if command == "list-secret-locations":
45
- return render_secret_locations(payload)
46
- if command == "secret-scan-history":
47
- return render_json_like(payload)
48
- if command == "export-alerts":
49
- return render_json_like(payload)
50
- if command == "api-call":
51
- return render_json_like(payload)
52
-
53
- return render_json_like(payload)
54
-
55
-
56
- def emit_output(payload: Any, *, as_json: bool, command: str) -> None:
57
- """Print output in JSON or text form."""
58
-
59
- if as_json:
60
- print(json.dumps(payload, indent=2, sort_keys=True))
61
- return
62
-
63
- text_payload = payload
64
- if command == "list-malware" and isinstance(payload, dict):
65
- text_payload = payload.get("alerts", [])
66
- print(render_text(command, text_payload))
67
- if (
68
- command == "list-malware"
69
- and isinstance(payload, dict)
70
- and payload.get("lookup_failures")
71
- ):
72
- print("\nMalware advisory lookup failures:")
73
- print(json.dumps(payload["lookup_failures"], indent=2, sort_keys=True))
74
-
75
-
76
- def render_summary(payload: dict[str, Any]) -> str:
77
- """Render a repository security summary."""
78
-
79
- lines = [
80
- f"Repository: {payload['full_name']}",
81
- f"Repository URL: {payload['repository_html_url']}",
82
- f"API base URL: {payload['api_base_url']}",
83
- f"Token environment variable: {payload['token_env']}",
84
- "",
85
- "Sections:",
86
- ]
87
-
88
- for section_name in (
89
- "code_scanning",
90
- "dependabot",
91
- "malware",
92
- "secret_scanning",
93
- ):
94
- section = payload["sections"][section_name]
95
- pretty_name = section_name.replace("_", " ")
96
- lines.append(f"- {pretty_name}:")
97
- if not section.get("ok"):
98
- error_payload = section.get("error", {})
99
- lines.append(
100
- " status: unavailable "
101
- f"({error_payload.get('status_code', 'n/a')}) "
102
- f"{error_payload.get('message', 'unknown error')}"
103
- )
104
- continue
105
-
106
- counts = section.get("counts_by_state", {})
107
- lines.append(f" total: {section.get('total', 0)}")
108
- if counts:
109
- lines.append(
110
- " counts: "
111
- + ", ".join(
112
- f"{state}={count}"
113
- for state, count in sorted(counts.items())
114
- )
115
- )
116
- samples = section.get("sample_alerts", [])
117
- if samples:
118
- lines.append(" samples:")
119
- for alert in samples:
120
- lines.append(
121
- f" - {render_alert_brief(alert, section_name)}"
122
- )
123
- lookup_failures = section.get("lookup_failures")
124
- if lookup_failures:
125
- lines.append(
126
- f" malware-type lookup failures: {len(lookup_failures)}"
127
- )
128
-
129
- return "\n".join(lines)
130
-
131
-
132
- def render_repo_security_overview(payload: dict[str, Any]) -> str:
133
- """Render repository settings overview."""
134
-
135
- lines = [
136
- f"Repository: {payload['full_name']}",
137
- f"Repository URL: {payload.get('html_url')}",
138
- f"Visibility: {payload.get('visibility')}",
139
- f"Private: {payload.get('private')}",
140
- f"API base URL: {payload['api_base_url']}",
141
- "",
142
- "security_and_analysis:",
143
- json.dumps(
144
- payload.get("security_and_analysis"), indent=2, sort_keys=True
145
- ),
146
- ]
147
- return "\n".join(lines)
148
-
149
-
150
- def render_code_scanning_list(alerts: list[dict[str, Any]]) -> str:
151
- """Render code scanning alerts as lines of text."""
152
-
153
- return render_alert_list(
154
- alerts, heading="Code scanning alerts", kind="code_scanning"
155
- )
156
-
157
-
158
- def render_dependabot_list(
159
- alerts: list[dict[str, Any]],
160
- *,
161
- heading: str = "Dependabot alerts",
162
- ) -> str:
163
- """Render Dependabot alerts as lines of text."""
164
-
165
- return render_alert_list(alerts, heading=heading, kind="dependabot")
166
-
167
-
168
- def render_secret_scanning_list(alerts: list[dict[str, Any]]) -> str:
169
- """Render secret scanning alerts as lines of text."""
170
-
171
- return render_alert_list(
172
- alerts, heading="Secret scanning alerts", kind="secret_scanning"
173
- )
174
-
175
-
176
- def render_secret_locations(locations: list[dict[str, Any]]) -> str:
177
- """Render secret scanning locations."""
178
-
179
- if not locations:
180
- return "No secret locations found."
181
-
182
- lines = ["Secret scanning alert locations:"]
183
- for location in locations:
184
- location_type = location.get("type", "unknown")
185
- details = location.get("details")
186
- if isinstance(details, dict):
187
- lines.append(
188
- f"- {location_type}: {json.dumps(details, sort_keys=True)}"
189
- )
190
- else:
191
- lines.append(f"- {location_type}: {details}")
192
-
193
- return "\n".join(lines)
194
-
195
-
196
- def render_alert_list(
197
- alerts: list[dict[str, Any]], *, heading: str, kind: str
198
- ) -> str:
199
- """Render a generic alert list with one alert per line."""
200
-
201
- if not alerts:
202
- return f"{heading}: none"
203
-
204
- lines = [f"{heading} ({len(alerts)}):"]
205
- for alert in alerts:
206
- lines.append(f"- {render_alert_brief(alert, kind)}")
207
- return "\n".join(lines)
208
-
209
-
210
- def render_alert_brief(alert: dict[str, Any], kind: str) -> str:
211
- """Create a one-line summary for one alert."""
212
-
213
- number = alert.get("number", "?")
214
- state = alert.get("state", "unknown")
215
- html_url = alert.get("html_url")
216
-
217
- if kind == "code_scanning":
218
- rule = (
219
- expect_dict(alert.get("rule") or {}, "code scanning rule")
220
- if alert.get("rule") is not None
221
- else {}
222
- )
223
- instance = (
224
- expect_dict(
225
- alert.get("most_recent_instance") or {},
226
- "code scanning instance",
227
- )
228
- if alert.get("most_recent_instance") is not None
229
- else {}
230
- )
231
- location = (
232
- expect_dict(
233
- instance.get("location") or {}, "code scanning location"
234
- )
235
- if instance.get("location") is not None
236
- else {}
237
- )
238
- severity = (
239
- rule.get("security_severity_level")
240
- or rule.get("severity")
241
- or "unknown"
242
- )
243
- rule_name = rule.get("id") or rule.get("name") or "unknown-rule"
244
- path = location.get("path") or "<unknown-path>"
245
- return (
246
- f"#{number} [{state}] severity={severity} rule={rule_name} "
247
- f"path={path} url={html_url}"
248
- )
249
-
250
- if kind in {"dependabot", "malware"}:
251
- vulnerability = (
252
- expect_dict(
253
- alert.get("security_vulnerability") or {},
254
- "Dependabot vulnerability",
255
- )
256
- if alert.get("security_vulnerability") is not None
257
- else {}
258
- )
259
- package = (
260
- expect_dict(
261
- vulnerability.get("package") or {}, "Dependabot package"
262
- )
263
- if vulnerability.get("package") is not None
264
- else {}
265
- )
266
- dependency = (
267
- expect_dict(alert.get("dependency") or {}, "Dependabot dependency")
268
- if alert.get("dependency") is not None
269
- else {}
270
- )
271
- severity = (
272
- vulnerability.get("severity") or alert.get("state") or "unknown"
273
- )
274
- if package.get("name") is not None:
275
- package_name = package.get("name")
276
- elif isinstance(dependency.get("package"), dict):
277
- package_name = dependency["package"].get("name")
278
- else:
279
- package_name = None
280
- manifest_path = dependency.get("manifest_path") or "<unknown-manifest>"
281
- ghsa_id = (
282
- alert.get("security_advisory", {}).get("ghsa_id")
283
- if isinstance(alert.get("security_advisory"), dict)
284
- else None
285
- )
286
- ghsa_id = ghsa_id or "unknown-ghsa"
287
- malware_suffix = " malware" if kind == "malware" else ""
288
- return (
289
- f"#{number} [{state}] severity={severity} package={package_name} "
290
- f"manifest={manifest_path} ghsa={ghsa_id}{malware_suffix} url={html_url}"
291
- )
292
-
293
- if kind == "secret_scanning":
294
- secret_type = alert.get("secret_type") or "unknown-secret-type"
295
- resolution = alert.get("resolution") or "unresolved"
296
- validity = alert.get("validity") or "unknown"
297
- leaked = alert.get("publicly_leaked")
298
- return (
299
- f"#{number} [{state}] secret_type={secret_type} resolution={resolution} "
300
- f"validity={validity} publicly_leaked={leaked} url={html_url}"
301
- )
302
-
303
- return f"#{number} [{state}] url={html_url}"
304
-
305
-
306
- def render_update_result(payload: dict[str, Any]) -> str:
307
- """Render dry-run or mutation results."""
308
-
309
- if payload.get("dry_run"):
310
- return "Dry run:\n" + json.dumps(payload, indent=2, sort_keys=True)
311
-
312
- return json.dumps(payload, indent=2, sort_keys=True)
313
-
314
-
315
- def render_json_like(payload: Any) -> str:
316
- """Render arbitrary payloads as pretty JSON."""
317
-
318
- return json.dumps(payload, indent=2, sort_keys=True)
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from github_security_common import expect_dict
7
+
8
+
9
+ def render_text(command: str, payload: Any) -> str:
10
+ """Render command output as human-readable text."""
11
+
12
+ renderers = {
13
+ "summary": render_summary,
14
+ "repo-security-overview": render_repo_security_overview,
15
+ "bulk-update-alerts": render_json_like,
16
+ "list-code-scanning": render_code_scanning_list,
17
+ "show-code-scanning": render_json_like,
18
+ "update-code-scanning": render_update_result,
19
+ "list-dependabot": render_dependabot_list,
20
+ "show-dependabot": render_json_like,
21
+ "update-dependabot": render_update_result,
22
+ "show-malware": render_json_like,
23
+ "update-malware": render_update_result,
24
+ "list-secret-scanning": render_secret_scanning_list,
25
+ "show-secret-scanning": render_json_like,
26
+ "update-secret-scanning": render_update_result,
27
+ "list-secret-locations": render_secret_locations,
28
+ "secret-scan-history": render_json_like,
29
+ "export-alerts": render_json_like,
30
+ "api-call": render_json_like,
31
+ }
32
+ if command == "list-malware":
33
+ return render_dependabot_list(payload, heading="Dependabot malware alerts")
34
+
35
+ return renderers.get(command, render_json_like)(payload)
36
+
37
+
38
+ def emit_output(payload: Any, *, as_json: bool, command: str) -> None:
39
+ """Print output in JSON or text form."""
40
+
41
+ if as_json:
42
+ print(json.dumps(payload, indent=2, sort_keys=True))
43
+ return
44
+
45
+ text_payload = payload
46
+ if command == "list-malware" and isinstance(payload, dict):
47
+ text_payload = payload.get("alerts", [])
48
+ print(render_text(command, text_payload))
49
+ if (
50
+ command == "list-malware"
51
+ and isinstance(payload, dict)
52
+ and payload.get("lookup_failures")
53
+ ):
54
+ print("\nMalware advisory lookup failures:")
55
+ print(json.dumps(payload["lookup_failures"], indent=2, sort_keys=True))
56
+
57
+
58
+ def render_summary(payload: dict[str, Any]) -> str:
59
+ """Render a repository security summary."""
60
+
61
+ lines = [
62
+ f"Repository: {payload['full_name']}",
63
+ f"Repository URL: {payload['repository_html_url']}",
64
+ f"API base URL: {payload['api_base_url']}",
65
+ f"Token environment variable: {payload['token_env']}",
66
+ "",
67
+ "Sections:",
68
+ ]
69
+
70
+ for section_name in (
71
+ "code_scanning",
72
+ "dependabot",
73
+ "malware",
74
+ "secret_scanning",
75
+ ):
76
+ append_summary_section(lines, section_name, payload["sections"][section_name])
77
+
78
+ return "\n".join(lines)
79
+
80
+
81
+ def append_summary_section(
82
+ lines: list[str], section_name: str, section: dict[str, Any]
83
+ ) -> None:
84
+ lines.append(f"- {section_name.replace('_', ' ')}:")
85
+ if not section.get("ok"):
86
+ append_unavailable_section(lines, section)
87
+ return
88
+
89
+ lines.append(f" total: {section.get('total', 0)}")
90
+ append_counts(lines, section.get("counts_by_state", {}))
91
+ append_sample_alerts(lines, section_name, section.get("sample_alerts", []))
92
+ lookup_failures = section.get("lookup_failures")
93
+ if lookup_failures:
94
+ lines.append(f" malware-type lookup failures: {len(lookup_failures)}")
95
+
96
+
97
+ def append_unavailable_section(lines: list[str], section: dict[str, Any]) -> None:
98
+ error_payload = section.get("error", {})
99
+ lines.append(
100
+ " status: unavailable "
101
+ f"({error_payload.get('status_code', 'n/a')}) "
102
+ f"{error_payload.get('message', 'unknown error')}"
103
+ )
104
+
105
+
106
+ def append_counts(lines: list[str], counts: Any) -> None:
107
+ if not counts:
108
+ return
109
+ lines.append(
110
+ " counts: "
111
+ + ", ".join(f"{state}={count}" for state, count in sorted(counts.items()))
112
+ )
113
+
114
+
115
+ def append_sample_alerts(
116
+ lines: list[str], section_name: str, samples: Any
117
+ ) -> None:
118
+ if not samples:
119
+ return
120
+
121
+ lines.append(" samples:")
122
+ for alert in samples:
123
+ lines.append(f" - {render_alert_brief(alert, section_name)}")
124
+
125
+
126
+ def render_repo_security_overview(payload: dict[str, Any]) -> str:
127
+ """Render repository settings overview."""
128
+
129
+ lines = [
130
+ f"Repository: {payload['full_name']}",
131
+ f"Repository URL: {payload.get('html_url')}",
132
+ f"Visibility: {payload.get('visibility')}",
133
+ f"Private: {payload.get('private')}",
134
+ f"API base URL: {payload['api_base_url']}",
135
+ "",
136
+ "security_and_analysis:",
137
+ json.dumps(
138
+ payload.get("security_and_analysis"), indent=2, sort_keys=True
139
+ ),
140
+ ]
141
+ return "\n".join(lines)
142
+
143
+
144
+ def render_code_scanning_list(alerts: list[dict[str, Any]]) -> str:
145
+ """Render code scanning alerts as lines of text."""
146
+
147
+ return render_alert_list(
148
+ alerts, heading="Code scanning alerts", kind="code_scanning"
149
+ )
150
+
151
+
152
+ def render_dependabot_list(
153
+ alerts: list[dict[str, Any]],
154
+ *,
155
+ heading: str = "Dependabot alerts",
156
+ ) -> str:
157
+ """Render Dependabot alerts as lines of text."""
158
+
159
+ return render_alert_list(alerts, heading=heading, kind="dependabot")
160
+
161
+
162
+ def render_secret_scanning_list(alerts: list[dict[str, Any]]) -> str:
163
+ """Render secret scanning alerts as lines of text."""
164
+
165
+ return render_alert_list(
166
+ alerts, heading="Secret scanning alerts", kind="secret_scanning"
167
+ )
168
+
169
+
170
+ def render_secret_locations(locations: list[dict[str, Any]]) -> str:
171
+ """Render secret scanning locations."""
172
+
173
+ if not locations:
174
+ return "No secret locations found."
175
+
176
+ lines = ["Secret scanning alert locations:"]
177
+ for location in locations:
178
+ location_type = location.get("type", "unknown")
179
+ details = location.get("details")
180
+ if isinstance(details, dict):
181
+ lines.append(
182
+ f"- {location_type}: {json.dumps(details, sort_keys=True)}"
183
+ )
184
+ else:
185
+ lines.append(f"- {location_type}: {details}")
186
+
187
+ return "\n".join(lines)
188
+
189
+
190
+ def render_alert_list(
191
+ alerts: list[dict[str, Any]], *, heading: str, kind: str
192
+ ) -> str:
193
+ """Render a generic alert list with one alert per line."""
194
+
195
+ if not alerts:
196
+ return f"{heading}: none"
197
+
198
+ lines = [f"{heading} ({len(alerts)}):"]
199
+ for alert in alerts:
200
+ lines.append(f"- {render_alert_brief(alert, kind)}")
201
+ return "\n".join(lines)
202
+
203
+
204
+ def render_alert_brief(alert: dict[str, Any], kind: str) -> str:
205
+ """Create a one-line summary for one alert."""
206
+
207
+ number = alert.get("number", "?")
208
+ state = alert.get("state", "unknown")
209
+ html_url = alert.get("html_url")
210
+
211
+ if kind == "code_scanning":
212
+ return render_code_scanning_brief(alert, number, state, html_url)
213
+
214
+ if kind in {"dependabot", "malware"}:
215
+ return render_dependabot_brief(alert, kind, number, state, html_url)
216
+
217
+ if kind == "secret_scanning":
218
+ return render_secret_scanning_brief(alert, number, state, html_url)
219
+
220
+ return f"#{number} [{state}] url={html_url}"
221
+
222
+
223
+ def render_code_scanning_brief(
224
+ alert: dict[str, Any], number: Any, state: Any, html_url: Any
225
+ ) -> str:
226
+ rule = optional_dict(alert.get("rule"), "code scanning rule")
227
+ instance = optional_dict(
228
+ alert.get("most_recent_instance"), "code scanning instance"
229
+ )
230
+ location = optional_dict(instance.get("location"), "code scanning location")
231
+ severity = rule.get("security_severity_level") or rule.get("severity") or "unknown"
232
+ rule_name = rule.get("id") or rule.get("name") or "unknown-rule"
233
+ path = location.get("path") or "<unknown-path>"
234
+ return (
235
+ f"#{number} [{state}] severity={severity} rule={rule_name} "
236
+ f"path={path} url={html_url}"
237
+ )
238
+
239
+
240
+ def render_dependabot_brief(
241
+ alert: dict[str, Any], kind: str, number: Any, state: Any, html_url: Any
242
+ ) -> str:
243
+ vulnerability = optional_dict(
244
+ alert.get("security_vulnerability"), "Dependabot vulnerability"
245
+ )
246
+ package = optional_dict(vulnerability.get("package"), "Dependabot package")
247
+ dependency = optional_dict(alert.get("dependency"), "Dependabot dependency")
248
+ severity = vulnerability.get("severity") or alert.get("state") or "unknown"
249
+ package_name = resolve_package_name(package, dependency)
250
+ manifest_path = dependency.get("manifest_path") or "<unknown-manifest>"
251
+ ghsa_id = resolve_ghsa_id(alert)
252
+ malware_suffix = " malware" if kind == "malware" else ""
253
+ return (
254
+ f"#{number} [{state}] severity={severity} package={package_name} "
255
+ f"manifest={manifest_path} ghsa={ghsa_id}{malware_suffix} url={html_url}"
256
+ )
257
+
258
+
259
+ def render_secret_scanning_brief(
260
+ alert: dict[str, Any], number: Any, state: Any, html_url: Any
261
+ ) -> str:
262
+ secret_type = alert.get("secret_type") or "unknown-secret-type"
263
+ resolution = alert.get("resolution") or "unresolved"
264
+ validity = alert.get("validity") or "unknown"
265
+ leaked = alert.get("publicly_leaked")
266
+ return (
267
+ f"#{number} [{state}] secret_type={secret_type} resolution={resolution} "
268
+ f"validity={validity} publicly_leaked={leaked} url={html_url}"
269
+ )
270
+
271
+
272
+ def optional_dict(value: Any, label: str) -> dict[str, Any]:
273
+ if value is None:
274
+ return {}
275
+ return expect_dict(value or {}, label)
276
+
277
+
278
+ def resolve_package_name(
279
+ package: dict[str, Any], dependency: dict[str, Any]
280
+ ) -> Any:
281
+ if package.get("name") is not None:
282
+ return package.get("name")
283
+
284
+ dependency_package = dependency.get("package")
285
+ if isinstance(dependency_package, dict):
286
+ return dependency_package.get("name")
287
+
288
+ return None
289
+
290
+
291
+ def resolve_ghsa_id(alert: dict[str, Any]) -> Any:
292
+ advisory = alert.get("security_advisory")
293
+ if isinstance(advisory, dict):
294
+ return advisory.get("ghsa_id") or "unknown-ghsa"
295
+ return "unknown-ghsa"
296
+
297
+
298
+ def render_update_result(payload: dict[str, Any]) -> str:
299
+ """Render dry-run or mutation results."""
300
+
301
+ if payload.get("dry_run"):
302
+ return "Dry run:\n" + json.dumps(payload, indent=2, sort_keys=True)
303
+
304
+ return json.dumps(payload, indent=2, sort_keys=True)
305
+
306
+
307
+ def render_json_like(payload: Any) -> str:
308
+ """Render arbitrary payloads as pretty JSON."""
309
+
310
+ return json.dumps(payload, indent=2, sort_keys=True)