github-manage-security-alerts-skill 1.0.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.
@@ -0,0 +1,318 @@
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)
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ """Inspect and manage GitHub repository security alerts."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ SCRIPT_DIR = Path(__file__).resolve().parent
11
+ if str(SCRIPT_DIR) not in sys.path:
12
+ sys.path.insert(0, str(SCRIPT_DIR))
13
+
14
+
15
+ def main() -> int:
16
+ """CLI entry point."""
17
+
18
+ from github_security_api import GitHubApiError, resolve_context
19
+ from github_security_cli import parse_args
20
+ from github_security_common import GitHubSecurityCliError
21
+ from github_security_operations import handle_command
22
+ from github_security_render import emit_output
23
+
24
+ arguments = parse_args()
25
+
26
+ try:
27
+ context = resolve_context(arguments)
28
+ payload = handle_command(context, arguments)
29
+ emit_output(payload, as_json=arguments.json, command=arguments.command)
30
+ except (
31
+ GitHubApiError,
32
+ GitHubSecurityCliError,
33
+ json.JSONDecodeError,
34
+ ) as exc:
35
+ if arguments.json:
36
+ print(
37
+ json.dumps(
38
+ {
39
+ "error": {
40
+ "command": arguments.command,
41
+ "message": str(exc),
42
+ "type": type(exc).__name__,
43
+ }
44
+ },
45
+ indent=2,
46
+ sort_keys=True,
47
+ ),
48
+ file=sys.stderr,
49
+ )
50
+ else:
51
+ print(f"Error: {exc}", file=sys.stderr)
52
+ return 1
53
+
54
+ return 0
55
+
56
+
57
+ if __name__ == "__main__":
58
+ raise SystemExit(main())