plugin-scanner 1.4.15__py3-none-any.whl

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.
Files changed (57) hide show
  1. codex_plugin_scanner/__init__.py +29 -0
  2. codex_plugin_scanner/action_runner.py +470 -0
  3. codex_plugin_scanner/checks/__init__.py +0 -0
  4. codex_plugin_scanner/checks/best_practices.py +238 -0
  5. codex_plugin_scanner/checks/claude.py +285 -0
  6. codex_plugin_scanner/checks/code_quality.py +115 -0
  7. codex_plugin_scanner/checks/ecosystem_common.py +34 -0
  8. codex_plugin_scanner/checks/gemini.py +196 -0
  9. codex_plugin_scanner/checks/manifest.py +501 -0
  10. codex_plugin_scanner/checks/manifest_support.py +61 -0
  11. codex_plugin_scanner/checks/marketplace.py +334 -0
  12. codex_plugin_scanner/checks/opencode.py +223 -0
  13. codex_plugin_scanner/checks/operational_security.py +346 -0
  14. codex_plugin_scanner/checks/security.py +447 -0
  15. codex_plugin_scanner/checks/skill_security.py +241 -0
  16. codex_plugin_scanner/cli.py +467 -0
  17. codex_plugin_scanner/config.py +76 -0
  18. codex_plugin_scanner/ecosystems/__init__.py +15 -0
  19. codex_plugin_scanner/ecosystems/base.py +20 -0
  20. codex_plugin_scanner/ecosystems/claude.py +112 -0
  21. codex_plugin_scanner/ecosystems/codex.py +94 -0
  22. codex_plugin_scanner/ecosystems/detect.py +46 -0
  23. codex_plugin_scanner/ecosystems/gemini.py +80 -0
  24. codex_plugin_scanner/ecosystems/opencode.py +184 -0
  25. codex_plugin_scanner/ecosystems/registry.py +41 -0
  26. codex_plugin_scanner/ecosystems/types.py +45 -0
  27. codex_plugin_scanner/integrations/__init__.py +5 -0
  28. codex_plugin_scanner/integrations/cisco_skill_scanner.py +200 -0
  29. codex_plugin_scanner/lint_fixes.py +105 -0
  30. codex_plugin_scanner/marketplace_support.py +100 -0
  31. codex_plugin_scanner/models.py +177 -0
  32. codex_plugin_scanner/path_support.py +46 -0
  33. codex_plugin_scanner/policy.py +140 -0
  34. codex_plugin_scanner/quality_artifact.py +91 -0
  35. codex_plugin_scanner/repo_detect.py +137 -0
  36. codex_plugin_scanner/reporting.py +376 -0
  37. codex_plugin_scanner/rules/__init__.py +6 -0
  38. codex_plugin_scanner/rules/registry.py +101 -0
  39. codex_plugin_scanner/rules/specs.py +26 -0
  40. codex_plugin_scanner/scanner.py +557 -0
  41. codex_plugin_scanner/submission.py +284 -0
  42. codex_plugin_scanner/suppressions.py +87 -0
  43. codex_plugin_scanner/trust_domain_scoring.py +22 -0
  44. codex_plugin_scanner/trust_helpers.py +207 -0
  45. codex_plugin_scanner/trust_mcp_scoring.py +116 -0
  46. codex_plugin_scanner/trust_models.py +85 -0
  47. codex_plugin_scanner/trust_plugin_scoring.py +180 -0
  48. codex_plugin_scanner/trust_scoring.py +52 -0
  49. codex_plugin_scanner/trust_skill_scoring.py +296 -0
  50. codex_plugin_scanner/trust_specs.py +286 -0
  51. codex_plugin_scanner/verification.py +964 -0
  52. codex_plugin_scanner/version.py +3 -0
  53. plugin_scanner-1.4.15.dist-info/METADATA +596 -0
  54. plugin_scanner-1.4.15.dist-info/RECORD +57 -0
  55. plugin_scanner-1.4.15.dist-info/WHEEL +4 -0
  56. plugin_scanner-1.4.15.dist-info/entry_points.txt +4 -0
  57. plugin_scanner-1.4.15.dist-info/licenses/LICENSE +120 -0
@@ -0,0 +1,29 @@
1
+ """Codex Plugin Scanner - multi-ecosystem scanner for agent plugin packages."""
2
+
3
+ from .models import (
4
+ GRADE_LABELS,
5
+ CategoryResult,
6
+ CheckResult,
7
+ Finding,
8
+ PackageSummary,
9
+ ScanOptions,
10
+ ScanResult,
11
+ Severity,
12
+ get_grade,
13
+ )
14
+ from .scanner import scan_plugin
15
+ from .version import __version__
16
+
17
+ __all__ = [
18
+ "GRADE_LABELS",
19
+ "CategoryResult",
20
+ "CheckResult",
21
+ "Finding",
22
+ "PackageSummary",
23
+ "ScanOptions",
24
+ "ScanResult",
25
+ "Severity",
26
+ "__version__",
27
+ "get_grade",
28
+ "scan_plugin",
29
+ ]
@@ -0,0 +1,470 @@
1
+ """GitHub Action entry point for scan and submission workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from . import __version__
12
+ from .cli import _build_plain_text, _build_verification_text, _scan_with_policy
13
+ from .models import GRADE_LABELS, max_severity
14
+ from .quality_artifact import build_quality_artifact, write_quality_artifact
15
+ from .reporting import build_json_payload, format_markdown, format_sarif, should_fail_for_severity
16
+ from .submission import (
17
+ SubmissionIssue,
18
+ build_submission_issue_body,
19
+ build_submission_issue_title,
20
+ build_submission_payload,
21
+ create_submission_issue,
22
+ find_existing_submission_issue,
23
+ resolve_submission_metadata,
24
+ )
25
+ from .verification import build_verification_payload, verify_plugin
26
+
27
+
28
+ def _parse_csv(value: str) -> tuple[str, ...]:
29
+ return tuple(item.strip() for item in value.split(",") if item.strip())
30
+
31
+
32
+ def _read_bool_env(name: str, *, default: bool = False) -> bool:
33
+ raw = os.environ.get(name)
34
+ if raw is None:
35
+ return default
36
+ return raw.strip().lower() == "true"
37
+
38
+
39
+ def _read_env(name: str, default: str = "") -> str:
40
+ return os.environ.get(name, default)
41
+
42
+
43
+ def _write_outputs(path: str, values: dict[str, str]) -> None:
44
+ with Path(path).open("a", encoding="utf-8") as handle:
45
+ for key, value in values.items():
46
+ handle.write(f"{key}={value}\n")
47
+
48
+
49
+ def _write_step_summary(path: str, lines: tuple[str, ...]) -> None:
50
+ with Path(path).open("a", encoding="utf-8") as handle:
51
+ handle.write("\n".join(lines))
52
+ handle.write("\n")
53
+
54
+
55
+ def _build_scan_args(
56
+ *,
57
+ plugin_dir: str,
58
+ profile: str,
59
+ config: str,
60
+ baseline: str,
61
+ min_score: int,
62
+ fail_on_severity: str,
63
+ cisco_scan: str,
64
+ cisco_policy: str,
65
+ ) -> argparse.Namespace:
66
+ return argparse.Namespace(
67
+ plugin_dir=plugin_dir,
68
+ profile=profile or None,
69
+ config=config or None,
70
+ baseline=baseline or None,
71
+ strict=False,
72
+ diff_base=None,
73
+ min_score=min_score,
74
+ fail_on_severity=fail_on_severity,
75
+ cisco_skill_scan=cisco_scan,
76
+ cisco_policy=cisco_policy,
77
+ )
78
+
79
+
80
+ def _render_scan_output(result, *, output_format: str, profile: str, policy_pass: bool, raw_score: int) -> str:
81
+ if output_format == "json":
82
+ return json.dumps(
83
+ build_json_payload(
84
+ result,
85
+ profile=profile,
86
+ policy_pass=policy_pass,
87
+ verify_pass=True,
88
+ raw_score=raw_score,
89
+ effective_score=result.score,
90
+ ),
91
+ indent=2,
92
+ )
93
+ if output_format == "markdown":
94
+ return format_markdown(result)
95
+ if output_format == "sarif":
96
+ return format_sarif(result)
97
+ return _build_plain_text(result)
98
+
99
+
100
+ def _render_verify_output(verification, *, output_format: str) -> str:
101
+ payload = build_verification_payload(verification)
102
+ if output_format == "json":
103
+ return json.dumps(payload, indent=2)
104
+ return _build_verification_text(payload)
105
+
106
+
107
+ def _render_lint_output(result, *, output_format: str, profile: str, policy_pass: bool) -> str:
108
+ if output_format == "json":
109
+ payload = {
110
+ "profile": profile,
111
+ "policy_pass": policy_pass,
112
+ "effective_score": result.score,
113
+ "findings": [
114
+ {
115
+ "rule_id": finding.rule_id,
116
+ "severity": finding.severity.value,
117
+ "category": finding.category,
118
+ "title": finding.title,
119
+ "description": finding.description,
120
+ }
121
+ for finding in result.findings
122
+ ],
123
+ }
124
+ return json.dumps(payload, indent=2)
125
+ lines = [f"Lint profile: {profile} | policy_pass={policy_pass} | effective_score={result.score}"]
126
+ for finding in result.findings:
127
+ lines.append(f"- {finding.rule_id} [{finding.severity.value}] {finding.title}")
128
+ return "\n".join(lines)
129
+
130
+
131
+ def _build_step_summary_lines(
132
+ *,
133
+ mode: str,
134
+ score: str,
135
+ grade: str,
136
+ grade_label: str,
137
+ max_severity: str,
138
+ findings_total: str,
139
+ report_path: str,
140
+ registry_payload_path: str,
141
+ submission_issues: list[SubmissionIssue],
142
+ submission_eligible: bool,
143
+ verify_pass: bool | None = None,
144
+ scope: str = "plugin",
145
+ local_plugin_count: int | None = None,
146
+ skipped_target_count: int | None = None,
147
+ ) -> tuple[str, ...]:
148
+ lines = ["## HOL Codex Plugin Scanner", "", f"- Mode: {mode}"]
149
+ lines.append(f"- Scope: {scope}")
150
+ if local_plugin_count is not None:
151
+ lines.append(f"- Local plugins scanned: {local_plugin_count}")
152
+ if skipped_target_count is not None:
153
+ lines.append(f"- Skipped marketplace entries: {skipped_target_count}")
154
+ if score:
155
+ lines.append(f"- Score: {score}/100")
156
+ if grade:
157
+ lines.append(f"- Grade: {grade} - {grade_label}")
158
+ if max_severity:
159
+ lines.append(f"- Max severity: {max_severity}")
160
+ if findings_total:
161
+ lines.append(f"- Findings: {findings_total}")
162
+ if verify_pass is not None:
163
+ lines.append(f"- Verification pass: {'yes' if verify_pass else 'no'}")
164
+ lines.append(f"- Submission eligible: {'yes' if submission_eligible else 'no'}")
165
+ if report_path:
166
+ lines.append(f"- Report: `{report_path}`")
167
+ if registry_payload_path:
168
+ lines.append(f"- Registry payload: `{registry_payload_path}`")
169
+ if submission_issues:
170
+ lines.append(f"- Submission issues: {', '.join(issue.url for issue in submission_issues)}")
171
+ return tuple(lines)
172
+
173
+
174
+ def main() -> int:
175
+ mode = _read_env("MODE", "scan")
176
+ plugin_dir = _read_env("PLUGIN_DIR", ".")
177
+ output_format = _read_env("FORMAT", "text")
178
+ output_path = _read_env("OUTPUT")
179
+ write_step_summary = _read_bool_env("WRITE_STEP_SUMMARY", default=True)
180
+ registry_payload_output = _read_env("REGISTRY_PAYLOAD_OUTPUT")
181
+ upload_sarif = _read_bool_env("UPLOAD_SARIF")
182
+ profile = _read_env("PROFILE", "default")
183
+ config = _read_env("CONFIG")
184
+ baseline = _read_env("BASELINE")
185
+ online = _read_bool_env("ONLINE")
186
+ min_score = int(_read_env("MIN_SCORE", "0"))
187
+ fail_on = _read_env("FAIL_ON", "none")
188
+ cisco_scan = _read_env("CISCO_SCAN", "auto")
189
+ cisco_policy = _read_env("CISCO_POLICY", "balanced")
190
+ submission_enabled = _read_bool_env("SUBMISSION_ENABLED")
191
+ submission_threshold = int(_read_env("SUBMISSION_SCORE_THRESHOLD", "80"))
192
+ submission_repos = _parse_csv(_read_env("SUBMISSION_REPOS"))
193
+ submission_token = _read_env("SUBMISSION_TOKEN").strip()
194
+ submission_labels = _parse_csv(_read_env("SUBMISSION_LABELS"))
195
+ submission_category = _read_env("SUBMISSION_CATEGORY", "Community Plugins")
196
+ submission_plugin_name = _read_env("SUBMISSION_PLUGIN_NAME")
197
+ submission_plugin_url = _read_env("SUBMISSION_PLUGIN_URL")
198
+ submission_plugin_description = _read_env("SUBMISSION_PLUGIN_DESCRIPTION")
199
+ submission_author = _read_env("SUBMISSION_AUTHOR")
200
+ github_repository = _read_env("GITHUB_REPOSITORY")
201
+ github_server_url = _read_env("GITHUB_SERVER_URL", "https://github.com")
202
+ github_sha = _read_env("GITHUB_SHA")
203
+ github_run_id = _read_env("GITHUB_RUN_ID")
204
+ github_api_url = _read_env("GITHUB_API_URL", "https://api.github.com")
205
+
206
+ workflow_url = ""
207
+ if github_repository and github_run_id:
208
+ workflow_url = f"{github_server_url.rstrip('/')}/{github_repository}/actions/runs/{github_run_id}"
209
+
210
+ report_path_value = ""
211
+ registry_payload_path_value = ""
212
+ submission_issues: list[SubmissionIssue] = []
213
+ submission_eligible = False
214
+ output_values = {
215
+ "mode": mode,
216
+ "score": "",
217
+ "grade": "",
218
+ "grade_label": "",
219
+ "policy_pass": "",
220
+ "verify_pass": "",
221
+ "max_severity": "",
222
+ "findings_total": "",
223
+ "report_path": "",
224
+ "registry_payload_path": "",
225
+ "submission_eligible": "false",
226
+ "submission_performed": "false",
227
+ "submission_issue_urls": "",
228
+ "submission_issue_numbers": "",
229
+ }
230
+ verify_pass_for_summary: bool | None = None
231
+ scan_scope = "plugin"
232
+ local_plugin_count: int | None = None
233
+ skipped_target_count: int | None = None
234
+
235
+ if mode in {"scan", "lint", "submit"}:
236
+ args = _build_scan_args(
237
+ plugin_dir=plugin_dir,
238
+ profile=profile,
239
+ config=config,
240
+ baseline=baseline,
241
+ min_score=min_score,
242
+ fail_on_severity=fail_on,
243
+ cisco_scan=cisco_scan,
244
+ cisco_policy=cisco_policy,
245
+ )
246
+ raw_result, result, resolved_profile, policy_eval, _effective_score = _scan_with_policy(
247
+ args,
248
+ Path(plugin_dir).resolve(),
249
+ )
250
+ scan_scope = getattr(result, "scope", "plugin")
251
+ if scan_scope == "repository":
252
+ local_plugin_count = len(result.plugin_results)
253
+ skipped_target_count = len(result.skipped_targets)
254
+ rendered = ""
255
+ artifact_path = ""
256
+ verification = None
257
+ if mode == "scan":
258
+ if upload_sarif:
259
+ if output_format != "sarif":
260
+ print("upload_sarif requires format=sarif.", file=sys.stderr)
261
+ return 1
262
+ if not output_path:
263
+ output_path = "codex-plugin-scanner.sarif"
264
+ rendered = _render_scan_output(
265
+ result,
266
+ output_format=output_format,
267
+ profile=resolved_profile,
268
+ policy_pass=policy_eval.policy_pass,
269
+ raw_score=raw_result.score,
270
+ )
271
+ elif mode == "lint":
272
+ rendered = _render_lint_output(
273
+ result,
274
+ output_format="json" if output_format not in {"json", "text"} else output_format,
275
+ profile=resolved_profile,
276
+ policy_pass=policy_eval.policy_pass,
277
+ )
278
+ else:
279
+ if scan_scope != "plugin":
280
+ print(
281
+ "Submission mode requires a single plugin directory. "
282
+ "Point plugin_dir at one plugin instead of a repo marketplace root.",
283
+ file=sys.stderr,
284
+ )
285
+ return 1
286
+ verification = verify_plugin(Path(plugin_dir).resolve(), online=online)
287
+ artifact_path = output_path or "plugin-quality.json"
288
+ artifact = build_quality_artifact(
289
+ Path(plugin_dir).resolve(),
290
+ result,
291
+ verification,
292
+ policy_eval,
293
+ resolved_profile,
294
+ raw_score=raw_result.score,
295
+ )
296
+ write_quality_artifact(Path(artifact_path), artifact)
297
+ rendered = json.dumps(artifact, indent=2)
298
+ print(f"Submission artifact written to {artifact_path}")
299
+ verify_pass_for_summary = verification.verify_pass
300
+
301
+ if output_path and mode != "submit":
302
+ target = Path(output_path)
303
+ target.write_text(rendered, encoding="utf-8")
304
+ print(f"Report written to {target}")
305
+ report_path_value = str(target)
306
+ elif mode == "submit":
307
+ report_path_value = artifact_path
308
+ else:
309
+ print(rendered)
310
+
311
+ severity_failed = should_fail_for_severity(result, fail_on)
312
+ output_values.update(
313
+ {
314
+ "score": str(result.score),
315
+ "grade": result.grade,
316
+ "grade_label": GRADE_LABELS.get(result.grade, "Unknown"),
317
+ "policy_pass": "true" if policy_eval.policy_pass else "false",
318
+ "verify_pass": "true" if verification is not None and verification.verify_pass else "",
319
+ "max_severity": max_severity(result.findings).value if result.findings else "none",
320
+ "findings_total": str(sum(result.severity_counts.values())),
321
+ }
322
+ )
323
+
324
+ if submission_enabled or registry_payload_output:
325
+ metadata = resolve_submission_metadata(
326
+ Path(plugin_dir).resolve(),
327
+ result,
328
+ plugin_name=submission_plugin_name,
329
+ plugin_url=submission_plugin_url,
330
+ description=submission_plugin_description,
331
+ author=submission_author,
332
+ category=submission_category,
333
+ github_repository=github_repository or None,
334
+ github_server_url=github_server_url,
335
+ )
336
+ registry_payload = build_submission_payload(
337
+ metadata,
338
+ result,
339
+ source_repository=github_repository,
340
+ source_sha=github_sha,
341
+ workflow_url=workflow_url,
342
+ scanner_version=__version__,
343
+ )
344
+ if registry_payload_output:
345
+ registry_path = Path(registry_payload_output)
346
+ registry_path.write_text(json.dumps(registry_payload, indent=2), encoding="utf-8")
347
+ registry_payload_path_value = str(registry_path)
348
+
349
+ verify_for_submission = verification.verify_pass if verification is not None else True
350
+ submission_eligible = (
351
+ submission_enabled
352
+ and result.score >= submission_threshold
353
+ and not severity_failed
354
+ and policy_eval.policy_pass
355
+ and verify_for_submission
356
+ )
357
+
358
+ if submission_eligible:
359
+ if not submission_repos:
360
+ print("Submission is enabled but no submission repositories were configured.", file=sys.stderr)
361
+ return 1
362
+ if not submission_token:
363
+ print("Submission is enabled but no submission token was provided.", file=sys.stderr)
364
+ return 1
365
+ if not metadata.plugin_url:
366
+ print("Submission metadata is missing a plugin repository URL.", file=sys.stderr)
367
+ return 1
368
+ title = build_submission_issue_title(metadata)
369
+ body = build_submission_issue_body(
370
+ metadata,
371
+ result,
372
+ payload=registry_payload,
373
+ workflow_url=workflow_url,
374
+ )
375
+ for submission_repo in submission_repos:
376
+ existing = find_existing_submission_issue(
377
+ submission_repo,
378
+ metadata.plugin_url,
379
+ submission_token,
380
+ api_base_url=github_api_url,
381
+ )
382
+ if existing is not None:
383
+ submission_issues.append(existing)
384
+ continue
385
+ submission_issues.append(
386
+ create_submission_issue(
387
+ submission_repo,
388
+ title,
389
+ body,
390
+ submission_token,
391
+ labels=submission_labels,
392
+ api_base_url=github_api_url,
393
+ )
394
+ )
395
+
396
+ output_values["submission_eligible"] = "true" if submission_eligible else "false"
397
+ output_values["submission_performed"] = "true" if submission_issues else "false"
398
+ output_values["submission_issue_urls"] = ",".join(issue.url for issue in submission_issues)
399
+ output_values["submission_issue_numbers"] = ",".join(str(issue.number) for issue in submission_issues)
400
+
401
+ if result.score < min_score:
402
+ print(f"Score {result.score} is below minimum threshold {min_score}", file=sys.stderr)
403
+ return 1
404
+ if should_fail_for_severity(result, fail_on):
405
+ print(f'Findings met or exceeded the "{fail_on}" severity threshold.', file=sys.stderr)
406
+ return 1
407
+ if not policy_eval.policy_pass:
408
+ print(f'Policy profile "{resolved_profile}" failed.', file=sys.stderr)
409
+ return 1
410
+ if mode == "submit" and verification is not None and not verification.verify_pass:
411
+ print("Submission blocked: runtime verification failed.", file=sys.stderr)
412
+ return 1
413
+
414
+ elif mode == "verify":
415
+ verification = verify_plugin(Path(plugin_dir).resolve(), online=online)
416
+ scan_scope = getattr(verification, "scope", "plugin")
417
+ if scan_scope == "repository":
418
+ local_plugin_count = len(verification.plugin_results)
419
+ skipped_target_count = len(verification.skipped_targets)
420
+ rendered = _render_verify_output(verification, output_format=output_format)
421
+ verify_pass_for_summary = verification.verify_pass
422
+ if output_path:
423
+ target = Path(output_path)
424
+ target.write_text(rendered, encoding="utf-8")
425
+ print(f"Report written to {target}")
426
+ report_path_value = str(target)
427
+ else:
428
+ print(rendered)
429
+ return_code = 1 if not verification.verify_pass else 0
430
+ output_values["verify_pass"] = "true" if verification.verify_pass else "false"
431
+ else:
432
+ print(f"Unsupported mode: {mode}", file=sys.stderr)
433
+ return 1
434
+
435
+ output_values["report_path"] = report_path_value
436
+ output_values["registry_payload_path"] = registry_payload_path_value
437
+
438
+ step_summary_path = _read_env("GITHUB_STEP_SUMMARY")
439
+ if write_step_summary and step_summary_path:
440
+ _write_step_summary(
441
+ step_summary_path,
442
+ _build_step_summary_lines(
443
+ mode=mode,
444
+ score=output_values["score"],
445
+ grade=output_values["grade"],
446
+ grade_label=output_values["grade_label"],
447
+ max_severity=output_values["max_severity"] or "none",
448
+ findings_total=output_values["findings_total"],
449
+ report_path=report_path_value,
450
+ registry_payload_path=registry_payload_path_value,
451
+ submission_issues=submission_issues,
452
+ submission_eligible=submission_eligible,
453
+ verify_pass=verify_pass_for_summary,
454
+ scope=scan_scope,
455
+ local_plugin_count=local_plugin_count,
456
+ skipped_target_count=skipped_target_count,
457
+ ),
458
+ )
459
+
460
+ github_output = _read_env("GITHUB_OUTPUT")
461
+ if github_output:
462
+ _write_outputs(github_output, output_values)
463
+
464
+ if mode == "verify":
465
+ return return_code
466
+ return 0
467
+
468
+
469
+ if __name__ == "__main__":
470
+ raise SystemExit(main())
File without changes