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.
- codex_plugin_scanner/__init__.py +29 -0
- codex_plugin_scanner/action_runner.py +470 -0
- codex_plugin_scanner/checks/__init__.py +0 -0
- codex_plugin_scanner/checks/best_practices.py +238 -0
- codex_plugin_scanner/checks/claude.py +285 -0
- codex_plugin_scanner/checks/code_quality.py +115 -0
- codex_plugin_scanner/checks/ecosystem_common.py +34 -0
- codex_plugin_scanner/checks/gemini.py +196 -0
- codex_plugin_scanner/checks/manifest.py +501 -0
- codex_plugin_scanner/checks/manifest_support.py +61 -0
- codex_plugin_scanner/checks/marketplace.py +334 -0
- codex_plugin_scanner/checks/opencode.py +223 -0
- codex_plugin_scanner/checks/operational_security.py +346 -0
- codex_plugin_scanner/checks/security.py +447 -0
- codex_plugin_scanner/checks/skill_security.py +241 -0
- codex_plugin_scanner/cli.py +467 -0
- codex_plugin_scanner/config.py +76 -0
- codex_plugin_scanner/ecosystems/__init__.py +15 -0
- codex_plugin_scanner/ecosystems/base.py +20 -0
- codex_plugin_scanner/ecosystems/claude.py +112 -0
- codex_plugin_scanner/ecosystems/codex.py +94 -0
- codex_plugin_scanner/ecosystems/detect.py +46 -0
- codex_plugin_scanner/ecosystems/gemini.py +80 -0
- codex_plugin_scanner/ecosystems/opencode.py +184 -0
- codex_plugin_scanner/ecosystems/registry.py +41 -0
- codex_plugin_scanner/ecosystems/types.py +45 -0
- codex_plugin_scanner/integrations/__init__.py +5 -0
- codex_plugin_scanner/integrations/cisco_skill_scanner.py +200 -0
- codex_plugin_scanner/lint_fixes.py +105 -0
- codex_plugin_scanner/marketplace_support.py +100 -0
- codex_plugin_scanner/models.py +177 -0
- codex_plugin_scanner/path_support.py +46 -0
- codex_plugin_scanner/policy.py +140 -0
- codex_plugin_scanner/quality_artifact.py +91 -0
- codex_plugin_scanner/repo_detect.py +137 -0
- codex_plugin_scanner/reporting.py +376 -0
- codex_plugin_scanner/rules/__init__.py +6 -0
- codex_plugin_scanner/rules/registry.py +101 -0
- codex_plugin_scanner/rules/specs.py +26 -0
- codex_plugin_scanner/scanner.py +557 -0
- codex_plugin_scanner/submission.py +284 -0
- codex_plugin_scanner/suppressions.py +87 -0
- codex_plugin_scanner/trust_domain_scoring.py +22 -0
- codex_plugin_scanner/trust_helpers.py +207 -0
- codex_plugin_scanner/trust_mcp_scoring.py +116 -0
- codex_plugin_scanner/trust_models.py +85 -0
- codex_plugin_scanner/trust_plugin_scoring.py +180 -0
- codex_plugin_scanner/trust_scoring.py +52 -0
- codex_plugin_scanner/trust_skill_scoring.py +296 -0
- codex_plugin_scanner/trust_specs.py +286 -0
- codex_plugin_scanner/verification.py +964 -0
- codex_plugin_scanner/version.py +3 -0
- plugin_scanner-1.4.15.dist-info/METADATA +596 -0
- plugin_scanner-1.4.15.dist-info/RECORD +57 -0
- plugin_scanner-1.4.15.dist-info/WHEEL +4 -0
- plugin_scanner-1.4.15.dist-info/entry_points.txt +4 -0
- plugin_scanner-1.4.15.dist-info/licenses/LICENSE +120 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Codex Plugin Scanner - core scanning engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .checks.best_practices import run_best_practice_checks
|
|
10
|
+
from .checks.claude import run_claude_checks
|
|
11
|
+
from .checks.code_quality import run_code_quality_checks
|
|
12
|
+
from .checks.gemini import run_gemini_checks
|
|
13
|
+
from .checks.manifest import run_manifest_checks
|
|
14
|
+
from .checks.marketplace import run_marketplace_checks
|
|
15
|
+
from .checks.opencode import run_opencode_checks
|
|
16
|
+
from .checks.operational_security import run_operational_security_checks
|
|
17
|
+
from .checks.security import run_security_checks
|
|
18
|
+
from .checks.skill_security import resolve_skill_security_context, run_skill_security_checks
|
|
19
|
+
from .ecosystems.detect import detect_packages
|
|
20
|
+
from .ecosystems.registry import get_default_adapters, resolve_ecosystem
|
|
21
|
+
from .ecosystems.types import Ecosystem, NormalizedPackage, PackageCandidate
|
|
22
|
+
from .integrations.cisco_skill_scanner import CiscoIntegrationStatus
|
|
23
|
+
from .models import (
|
|
24
|
+
CategoryResult,
|
|
25
|
+
CheckResult,
|
|
26
|
+
Finding,
|
|
27
|
+
IntegrationResult,
|
|
28
|
+
PackageSummary,
|
|
29
|
+
ScanOptions,
|
|
30
|
+
ScanResult,
|
|
31
|
+
build_severity_counts,
|
|
32
|
+
get_grade,
|
|
33
|
+
)
|
|
34
|
+
from .repo_detect import LocalPluginTarget, discover_scan_targets
|
|
35
|
+
from .trust_scoring import build_plugin_trust_report, build_repository_trust_report
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_integration_results(skill_security_context, package_label: str = "") -> tuple[IntegrationResult, ...]:
|
|
39
|
+
integration_name = "cisco-skill-scanner" if not package_label else f"cisco-skill-scanner[{package_label}]"
|
|
40
|
+
if skill_security_context.skip_message:
|
|
41
|
+
return (
|
|
42
|
+
IntegrationResult(
|
|
43
|
+
name=integration_name,
|
|
44
|
+
status=CiscoIntegrationStatus.SKIPPED,
|
|
45
|
+
message=skill_security_context.skip_message,
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
summary = skill_security_context.summary
|
|
50
|
+
if summary is None:
|
|
51
|
+
return (
|
|
52
|
+
IntegrationResult(
|
|
53
|
+
name=integration_name,
|
|
54
|
+
status=CiscoIntegrationStatus.SKIPPED,
|
|
55
|
+
message="Cisco scan context unavailable.",
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
metadata = {"policy": summary.policy_name}
|
|
60
|
+
if summary.analyzers_used:
|
|
61
|
+
metadata["analyzers"] = ",".join(summary.analyzers_used)
|
|
62
|
+
return (
|
|
63
|
+
IntegrationResult(
|
|
64
|
+
name=integration_name,
|
|
65
|
+
status=summary.status,
|
|
66
|
+
message=summary.message,
|
|
67
|
+
findings_count=summary.total_findings,
|
|
68
|
+
metadata=metadata,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _score_categories(categories: tuple[CategoryResult, ...]) -> int:
|
|
74
|
+
earned_points = sum(check.points for category in categories for check in category.checks)
|
|
75
|
+
max_points = sum(check.max_points for category in categories for check in category.checks)
|
|
76
|
+
return 100 if max_points == 0 else round((earned_points / max_points) * 100)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build_adapter_map() -> dict[Ecosystem, object]:
|
|
80
|
+
adapters = get_default_adapters()
|
|
81
|
+
return {adapter.ecosystem_id: adapter for adapter in adapters}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_candidate_fallback(resolved: Path, requested: Ecosystem | None) -> list[PackageCandidate]:
|
|
85
|
+
if requested is not None:
|
|
86
|
+
return [
|
|
87
|
+
PackageCandidate(
|
|
88
|
+
ecosystem=requested,
|
|
89
|
+
package_kind="workspace-bundle",
|
|
90
|
+
root_path=resolved,
|
|
91
|
+
manifest_path=None,
|
|
92
|
+
detection_reason="fallback to requested ecosystem at scan root",
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
return [
|
|
96
|
+
PackageCandidate(
|
|
97
|
+
ecosystem=Ecosystem.CODEX,
|
|
98
|
+
package_kind="single-plugin",
|
|
99
|
+
root_path=resolved,
|
|
100
|
+
manifest_path=resolved / ".codex-plugin" / "plugin.json",
|
|
101
|
+
detection_reason="legacy codex fallback at scan root",
|
|
102
|
+
)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _category_prefix(scan_root: Path, package: NormalizedPackage, package_count: int) -> str:
|
|
107
|
+
if package_count == 1 and package.root_path.resolve() == scan_root.resolve():
|
|
108
|
+
return ""
|
|
109
|
+
try:
|
|
110
|
+
relative_root = package.root_path.resolve().relative_to(scan_root.resolve())
|
|
111
|
+
relative_label = "." if str(relative_root) == "." else str(relative_root)
|
|
112
|
+
except ValueError:
|
|
113
|
+
relative_label = str(package.root_path)
|
|
114
|
+
return f"[{package.ecosystem.value}:{relative_label}] "
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_path_within(path: Path, root: Path) -> bool:
|
|
118
|
+
try:
|
|
119
|
+
path.relative_to(root)
|
|
120
|
+
return True
|
|
121
|
+
except ValueError:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _summarize_package(package: NormalizedPackage) -> PackageSummary:
|
|
126
|
+
return PackageSummary(
|
|
127
|
+
ecosystem=package.ecosystem.value,
|
|
128
|
+
package_kind=package.package_kind,
|
|
129
|
+
root_path=str(package.root_path),
|
|
130
|
+
manifest_path=str(package.manifest_path) if package.manifest_path is not None else None,
|
|
131
|
+
name=package.name,
|
|
132
|
+
version=package.version,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _rebase_finding(finding: Finding, plugin_dir: Path, repo_root: Path) -> Finding:
|
|
137
|
+
if not finding.file_path:
|
|
138
|
+
return finding
|
|
139
|
+
file_path = Path(finding.file_path)
|
|
140
|
+
resolved_path = file_path.resolve() if file_path.is_absolute() else (plugin_dir / file_path).resolve()
|
|
141
|
+
try:
|
|
142
|
+
rebased_path = resolved_path.relative_to(repo_root.resolve()).as_posix()
|
|
143
|
+
except ValueError:
|
|
144
|
+
return finding
|
|
145
|
+
return replace(finding, file_path=rebased_path)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _rebase_check_result(check: CheckResult, plugin_dir: Path, repo_root: Path) -> CheckResult:
|
|
149
|
+
return replace(
|
|
150
|
+
check,
|
|
151
|
+
findings=tuple(_rebase_finding(finding, plugin_dir, repo_root) for finding in check.findings),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _maybe_rebase_checks(
|
|
156
|
+
checks: tuple[CheckResult, ...],
|
|
157
|
+
plugin_dir: Path,
|
|
158
|
+
repo_root: Path,
|
|
159
|
+
enabled: bool,
|
|
160
|
+
) -> tuple[CheckResult, ...]:
|
|
161
|
+
if not enabled:
|
|
162
|
+
return checks
|
|
163
|
+
return tuple(_rebase_check_result(check, plugin_dir, repo_root) for check in checks)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _rebase_plugin_result(plugin_result: ScanResult, plugin_target: LocalPluginTarget, repo_root: Path) -> ScanResult:
|
|
167
|
+
rebased_categories = tuple(
|
|
168
|
+
CategoryResult(
|
|
169
|
+
name=category.name,
|
|
170
|
+
checks=tuple(_rebase_check_result(check, plugin_target.plugin_dir, repo_root) for check in category.checks),
|
|
171
|
+
)
|
|
172
|
+
for category in plugin_result.categories
|
|
173
|
+
)
|
|
174
|
+
rebased_integrations = tuple(
|
|
175
|
+
replace(integration, name=f"{plugin_target.name} / {integration.name}")
|
|
176
|
+
for integration in plugin_result.integrations
|
|
177
|
+
)
|
|
178
|
+
rebased_findings = tuple(
|
|
179
|
+
_rebase_finding(finding, plugin_target.plugin_dir, repo_root) for finding in plugin_result.findings
|
|
180
|
+
)
|
|
181
|
+
return replace(
|
|
182
|
+
plugin_result,
|
|
183
|
+
categories=rebased_categories,
|
|
184
|
+
findings=rebased_findings,
|
|
185
|
+
severity_counts=build_severity_counts(rebased_findings),
|
|
186
|
+
integrations=rebased_integrations,
|
|
187
|
+
plugin_name=plugin_target.name,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _scan_single_plugin(plugin_dir: Path, options: ScanOptions) -> ScanResult:
|
|
192
|
+
skill_security_context = resolve_skill_security_context(plugin_dir, options)
|
|
193
|
+
categories: list[CategoryResult] = [
|
|
194
|
+
CategoryResult(name="Manifest Validation", checks=run_manifest_checks(plugin_dir)),
|
|
195
|
+
CategoryResult(name="Security", checks=run_security_checks(plugin_dir)),
|
|
196
|
+
CategoryResult(name="Operational Security", checks=run_operational_security_checks(plugin_dir)),
|
|
197
|
+
CategoryResult(name="Best Practices", checks=run_best_practice_checks(plugin_dir)),
|
|
198
|
+
CategoryResult(name="Marketplace", checks=run_marketplace_checks(plugin_dir)),
|
|
199
|
+
CategoryResult(
|
|
200
|
+
name="Skill Security",
|
|
201
|
+
checks=run_skill_security_checks(plugin_dir, options, skill_security_context),
|
|
202
|
+
),
|
|
203
|
+
CategoryResult(name="Code Quality", checks=run_code_quality_checks(plugin_dir)),
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
score = _score_categories(tuple(categories))
|
|
207
|
+
findings = tuple(finding for category in categories for check in category.checks for finding in check.findings)
|
|
208
|
+
trust_report = build_plugin_trust_report(plugin_dir, tuple(categories), skill_security_context)
|
|
209
|
+
return ScanResult(
|
|
210
|
+
score=score,
|
|
211
|
+
grade=get_grade(score),
|
|
212
|
+
categories=tuple(categories),
|
|
213
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
214
|
+
plugin_dir=str(plugin_dir),
|
|
215
|
+
findings=findings,
|
|
216
|
+
severity_counts=build_severity_counts(findings),
|
|
217
|
+
integrations=_build_integration_results(skill_security_context),
|
|
218
|
+
scope="plugin",
|
|
219
|
+
trust_report=trust_report,
|
|
220
|
+
ecosystems=("codex",),
|
|
221
|
+
packages=(
|
|
222
|
+
PackageSummary(
|
|
223
|
+
ecosystem="codex",
|
|
224
|
+
package_kind="single-plugin",
|
|
225
|
+
root_path=str(plugin_dir),
|
|
226
|
+
manifest_path=str(plugin_dir / ".codex-plugin" / "plugin.json"),
|
|
227
|
+
name=None,
|
|
228
|
+
version=None,
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _build_repository_categories(
|
|
235
|
+
repo_root: Path,
|
|
236
|
+
plugin_results: tuple[ScanResult, ...],
|
|
237
|
+
) -> tuple[CategoryResult, ...]:
|
|
238
|
+
categories: list[CategoryResult] = [
|
|
239
|
+
CategoryResult(name="Repository Marketplace", checks=run_marketplace_checks(repo_root)),
|
|
240
|
+
CategoryResult(name="Repository Operational Security", checks=run_operational_security_checks(repo_root)),
|
|
241
|
+
]
|
|
242
|
+
for plugin_result in plugin_results:
|
|
243
|
+
for category in plugin_result.categories:
|
|
244
|
+
if category.name in {"Marketplace", "Operational Security"}:
|
|
245
|
+
continue
|
|
246
|
+
plugin_name = plugin_result.plugin_name or Path(plugin_result.plugin_dir).name
|
|
247
|
+
categories.append(CategoryResult(name=f"{plugin_name} · {category.name}", checks=category.checks))
|
|
248
|
+
return tuple(categories)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _scan_repository(repo_root: Path, options: ScanOptions) -> ScanResult:
|
|
252
|
+
discovery = discover_scan_targets(repo_root)
|
|
253
|
+
plugin_results = tuple(
|
|
254
|
+
_rebase_plugin_result(_scan_single_plugin(target.plugin_dir, options), target, repo_root)
|
|
255
|
+
for target in discovery.local_plugins
|
|
256
|
+
)
|
|
257
|
+
categories = _build_repository_categories(repo_root, plugin_results)
|
|
258
|
+
findings = tuple(finding for category in categories for check in category.checks for finding in check.findings)
|
|
259
|
+
repo_scores = [plugin.score for plugin in plugin_results]
|
|
260
|
+
repo_category_score = _score_categories(categories[:2]) if categories[:2] else 100
|
|
261
|
+
if categories[:2]:
|
|
262
|
+
repo_scores.append(repo_category_score)
|
|
263
|
+
score = min(repo_scores) if repo_scores else 0
|
|
264
|
+
trust_report = build_repository_trust_report(
|
|
265
|
+
tuple(plugin.trust_report for plugin in plugin_results if plugin.trust_report is not None)
|
|
266
|
+
)
|
|
267
|
+
ecosystems = tuple(sorted({ecosystem for plugin in plugin_results for ecosystem in plugin.ecosystems})) or (
|
|
268
|
+
"codex",
|
|
269
|
+
)
|
|
270
|
+
packages = tuple(summary for plugin in plugin_results for summary in plugin.packages)
|
|
271
|
+
return ScanResult(
|
|
272
|
+
score=score,
|
|
273
|
+
grade=get_grade(score),
|
|
274
|
+
categories=categories,
|
|
275
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
276
|
+
plugin_dir=str(repo_root),
|
|
277
|
+
findings=findings,
|
|
278
|
+
severity_counts=build_severity_counts(findings),
|
|
279
|
+
integrations=tuple(integration for plugin in plugin_results for integration in plugin.integrations),
|
|
280
|
+
scope="repository",
|
|
281
|
+
plugin_results=plugin_results,
|
|
282
|
+
skipped_targets=discovery.skipped_targets,
|
|
283
|
+
marketplace_file=str(discovery.marketplace_file) if discovery.marketplace_file else None,
|
|
284
|
+
trust_report=trust_report,
|
|
285
|
+
ecosystems=ecosystems,
|
|
286
|
+
packages=packages,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _scan_mixed_packages(scan_root: Path, packages: list[NormalizedPackage], options: ScanOptions) -> ScanResult:
|
|
291
|
+
package_count = len(packages)
|
|
292
|
+
scan_root_resolved = scan_root.resolve()
|
|
293
|
+
categories: list[CategoryResult] = []
|
|
294
|
+
integrations: list[IntegrationResult] = []
|
|
295
|
+
plugin_results: list[ScanResult] = []
|
|
296
|
+
skipped_targets = []
|
|
297
|
+
codex_trust_reports = []
|
|
298
|
+
processed_packages: list[NormalizedPackage] = []
|
|
299
|
+
codex_marketplace_roots = tuple(
|
|
300
|
+
package.root_path.resolve()
|
|
301
|
+
for package in packages
|
|
302
|
+
if package.ecosystem == Ecosystem.CODEX and package.package_kind == "marketplace"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
for package in packages:
|
|
306
|
+
package_root = package.root_path.resolve()
|
|
307
|
+
if (
|
|
308
|
+
package.ecosystem == Ecosystem.CODEX
|
|
309
|
+
and package.package_kind == "single-plugin"
|
|
310
|
+
and any(
|
|
311
|
+
package_root != marketplace_root and _is_path_within(package_root, marketplace_root)
|
|
312
|
+
for marketplace_root in codex_marketplace_roots
|
|
313
|
+
)
|
|
314
|
+
):
|
|
315
|
+
continue
|
|
316
|
+
needs_rebase = package_root != scan_root_resolved
|
|
317
|
+
prefix = _category_prefix(scan_root, package, package_count)
|
|
318
|
+
|
|
319
|
+
if package.ecosystem == Ecosystem.CODEX:
|
|
320
|
+
if package.package_kind == "marketplace":
|
|
321
|
+
codex_repo_result = _scan_repository(package_root, options)
|
|
322
|
+
codex_categories = list(codex_repo_result.categories)
|
|
323
|
+
codex_integrations = list(codex_repo_result.integrations)
|
|
324
|
+
if prefix:
|
|
325
|
+
codex_categories = [
|
|
326
|
+
CategoryResult(name=f"{prefix}{category.name}", checks=category.checks)
|
|
327
|
+
for category in codex_categories
|
|
328
|
+
]
|
|
329
|
+
codex_integrations = [
|
|
330
|
+
replace(
|
|
331
|
+
integration,
|
|
332
|
+
name=f"{package.ecosystem.value}:{package_root.name} / {integration.name}",
|
|
333
|
+
)
|
|
334
|
+
for integration in codex_integrations
|
|
335
|
+
]
|
|
336
|
+
categories.extend(codex_categories)
|
|
337
|
+
integrations.extend(codex_integrations)
|
|
338
|
+
if codex_repo_result.trust_report is not None:
|
|
339
|
+
codex_trust_reports.append(codex_repo_result.trust_report)
|
|
340
|
+
plugin_results.extend(codex_repo_result.plugin_results)
|
|
341
|
+
skipped_targets.extend(codex_repo_result.skipped_targets)
|
|
342
|
+
processed_packages.append(package)
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
codex_result = _scan_single_plugin(package_root, options)
|
|
346
|
+
package_name = package.name or package_root.name
|
|
347
|
+
if needs_rebase:
|
|
348
|
+
codex_plugin_result = _rebase_plugin_result(
|
|
349
|
+
codex_result,
|
|
350
|
+
LocalPluginTarget(name=package_name, plugin_dir=package_root, source_path="./"),
|
|
351
|
+
scan_root_resolved,
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
codex_plugin_result = replace(codex_result, plugin_name=package_name)
|
|
355
|
+
codex_categories = [
|
|
356
|
+
CategoryResult(
|
|
357
|
+
name=category.name,
|
|
358
|
+
checks=_maybe_rebase_checks(category.checks, package_root, scan_root_resolved, needs_rebase),
|
|
359
|
+
)
|
|
360
|
+
for category in codex_result.categories
|
|
361
|
+
]
|
|
362
|
+
codex_integrations = list(codex_result.integrations)
|
|
363
|
+
if prefix:
|
|
364
|
+
codex_categories = [
|
|
365
|
+
CategoryResult(name=f"{prefix}{category.name}", checks=category.checks)
|
|
366
|
+
for category in codex_categories
|
|
367
|
+
]
|
|
368
|
+
codex_integrations = [
|
|
369
|
+
replace(
|
|
370
|
+
integration,
|
|
371
|
+
name=f"{package.ecosystem.value}:{package_root.name} / {integration.name}",
|
|
372
|
+
)
|
|
373
|
+
for integration in codex_integrations
|
|
374
|
+
]
|
|
375
|
+
categories.extend(codex_categories)
|
|
376
|
+
integrations.extend(codex_integrations)
|
|
377
|
+
if codex_result.trust_report is not None:
|
|
378
|
+
codex_trust_reports.append(codex_result.trust_report)
|
|
379
|
+
plugin_results.append(codex_plugin_result)
|
|
380
|
+
processed_packages.append(package)
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
if package.ecosystem == Ecosystem.CLAUDE:
|
|
384
|
+
claude_checks = _maybe_rebase_checks(
|
|
385
|
+
run_claude_checks(package),
|
|
386
|
+
package_root,
|
|
387
|
+
scan_root_resolved,
|
|
388
|
+
needs_rebase,
|
|
389
|
+
)
|
|
390
|
+
security_checks = _maybe_rebase_checks(
|
|
391
|
+
run_security_checks(package_root),
|
|
392
|
+
package_root,
|
|
393
|
+
scan_root_resolved,
|
|
394
|
+
needs_rebase,
|
|
395
|
+
)
|
|
396
|
+
operational_checks = _maybe_rebase_checks(
|
|
397
|
+
run_operational_security_checks(package_root),
|
|
398
|
+
package_root,
|
|
399
|
+
scan_root_resolved,
|
|
400
|
+
needs_rebase,
|
|
401
|
+
)
|
|
402
|
+
quality_checks = _maybe_rebase_checks(
|
|
403
|
+
run_code_quality_checks(package_root),
|
|
404
|
+
package_root,
|
|
405
|
+
scan_root_resolved,
|
|
406
|
+
needs_rebase,
|
|
407
|
+
)
|
|
408
|
+
categories.extend(
|
|
409
|
+
(
|
|
410
|
+
CategoryResult(name=f"{prefix}Claude Plugin", checks=claude_checks),
|
|
411
|
+
CategoryResult(name=f"{prefix}Security", checks=security_checks),
|
|
412
|
+
CategoryResult(name=f"{prefix}Operational Security", checks=operational_checks),
|
|
413
|
+
CategoryResult(name=f"{prefix}Code Quality", checks=quality_checks),
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
processed_packages.append(package)
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
if package.ecosystem == Ecosystem.GEMINI:
|
|
420
|
+
gemini_checks = _maybe_rebase_checks(
|
|
421
|
+
run_gemini_checks(package),
|
|
422
|
+
package_root,
|
|
423
|
+
scan_root_resolved,
|
|
424
|
+
needs_rebase,
|
|
425
|
+
)
|
|
426
|
+
security_checks = _maybe_rebase_checks(
|
|
427
|
+
run_security_checks(package_root),
|
|
428
|
+
package_root,
|
|
429
|
+
scan_root_resolved,
|
|
430
|
+
needs_rebase,
|
|
431
|
+
)
|
|
432
|
+
operational_checks = _maybe_rebase_checks(
|
|
433
|
+
run_operational_security_checks(package_root),
|
|
434
|
+
package_root,
|
|
435
|
+
scan_root_resolved,
|
|
436
|
+
needs_rebase,
|
|
437
|
+
)
|
|
438
|
+
quality_checks = _maybe_rebase_checks(
|
|
439
|
+
run_code_quality_checks(package_root),
|
|
440
|
+
package_root,
|
|
441
|
+
scan_root_resolved,
|
|
442
|
+
needs_rebase,
|
|
443
|
+
)
|
|
444
|
+
categories.extend(
|
|
445
|
+
(
|
|
446
|
+
CategoryResult(name=f"{prefix}Gemini Extension", checks=gemini_checks),
|
|
447
|
+
CategoryResult(name=f"{prefix}Security", checks=security_checks),
|
|
448
|
+
CategoryResult(name=f"{prefix}Operational Security", checks=operational_checks),
|
|
449
|
+
CategoryResult(name=f"{prefix}Code Quality", checks=quality_checks),
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
processed_packages.append(package)
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
if package.ecosystem == Ecosystem.OPENCODE:
|
|
456
|
+
opencode_checks = _maybe_rebase_checks(
|
|
457
|
+
run_opencode_checks(package),
|
|
458
|
+
package_root,
|
|
459
|
+
scan_root_resolved,
|
|
460
|
+
needs_rebase,
|
|
461
|
+
)
|
|
462
|
+
security_checks = _maybe_rebase_checks(
|
|
463
|
+
run_security_checks(package_root),
|
|
464
|
+
package_root,
|
|
465
|
+
scan_root_resolved,
|
|
466
|
+
needs_rebase,
|
|
467
|
+
)
|
|
468
|
+
operational_checks = _maybe_rebase_checks(
|
|
469
|
+
run_operational_security_checks(package_root),
|
|
470
|
+
package_root,
|
|
471
|
+
scan_root_resolved,
|
|
472
|
+
needs_rebase,
|
|
473
|
+
)
|
|
474
|
+
quality_checks = _maybe_rebase_checks(
|
|
475
|
+
run_code_quality_checks(package_root),
|
|
476
|
+
package_root,
|
|
477
|
+
scan_root_resolved,
|
|
478
|
+
needs_rebase,
|
|
479
|
+
)
|
|
480
|
+
categories.extend(
|
|
481
|
+
(
|
|
482
|
+
CategoryResult(name=f"{prefix}OpenCode Plugin", checks=opencode_checks),
|
|
483
|
+
CategoryResult(name=f"{prefix}Security", checks=security_checks),
|
|
484
|
+
CategoryResult(name=f"{prefix}Operational Security", checks=operational_checks),
|
|
485
|
+
CategoryResult(name=f"{prefix}Code Quality", checks=quality_checks),
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
processed_packages.append(package)
|
|
489
|
+
|
|
490
|
+
findings = tuple(finding for category in categories for check in category.checks for finding in check.findings)
|
|
491
|
+
score = _score_categories(tuple(categories))
|
|
492
|
+
trust_report = build_repository_trust_report(tuple(codex_trust_reports)) if codex_trust_reports else None
|
|
493
|
+
reported_packages = tuple(processed_packages) if processed_packages else tuple(packages)
|
|
494
|
+
marketplace_candidates = tuple(
|
|
495
|
+
package
|
|
496
|
+
for package in reported_packages
|
|
497
|
+
if package.ecosystem == Ecosystem.CODEX and package.package_kind == "marketplace"
|
|
498
|
+
)
|
|
499
|
+
scope = "repository" if marketplace_candidates or len(reported_packages) > 1 else "plugin"
|
|
500
|
+
marketplace_file = str(marketplace_candidates[0].manifest_path) if len(marketplace_candidates) == 1 else None
|
|
501
|
+
return ScanResult(
|
|
502
|
+
score=score,
|
|
503
|
+
grade=get_grade(score),
|
|
504
|
+
categories=tuple(categories),
|
|
505
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
506
|
+
plugin_dir=str(scan_root),
|
|
507
|
+
findings=findings,
|
|
508
|
+
severity_counts=build_severity_counts(findings),
|
|
509
|
+
integrations=tuple(integrations),
|
|
510
|
+
scope=scope,
|
|
511
|
+
plugin_results=tuple(plugin_results),
|
|
512
|
+
skipped_targets=tuple(skipped_targets),
|
|
513
|
+
marketplace_file=marketplace_file,
|
|
514
|
+
trust_report=trust_report,
|
|
515
|
+
ecosystems=tuple(sorted({package.ecosystem.value for package in reported_packages})),
|
|
516
|
+
packages=tuple(_summarize_package(package) for package in reported_packages),
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _scan_non_repository_target(target_dir: Path, options: ScanOptions) -> ScanResult:
|
|
521
|
+
requested_ecosystem = resolve_ecosystem(options.ecosystem)
|
|
522
|
+
candidates = detect_packages(target_dir, requested_ecosystem)
|
|
523
|
+
if not candidates:
|
|
524
|
+
candidates = _build_candidate_fallback(target_dir, requested_ecosystem)
|
|
525
|
+
|
|
526
|
+
adapter_map = _build_adapter_map()
|
|
527
|
+
packages = [adapter_map[candidate.ecosystem].parse(candidate) for candidate in candidates]
|
|
528
|
+
|
|
529
|
+
if (
|
|
530
|
+
len(packages) == 1
|
|
531
|
+
and packages[0].ecosystem == Ecosystem.CODEX
|
|
532
|
+
and packages[0].root_path.resolve() == target_dir.resolve()
|
|
533
|
+
):
|
|
534
|
+
codex_result = _scan_single_plugin(target_dir, options)
|
|
535
|
+
summary = _summarize_package(packages[0])
|
|
536
|
+
return replace(codex_result, ecosystems=("codex",), packages=(summary,))
|
|
537
|
+
|
|
538
|
+
return _scan_mixed_packages(target_dir, packages, options)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def scan_plugin(plugin_dir: str | Path, options: ScanOptions | None = None) -> ScanResult:
|
|
542
|
+
"""Scan a plugin directory or repository root."""
|
|
543
|
+
|
|
544
|
+
resolved = Path(plugin_dir).resolve()
|
|
545
|
+
scan_options = options or ScanOptions()
|
|
546
|
+
requested_ecosystem = resolve_ecosystem(scan_options.ecosystem)
|
|
547
|
+
discovery = discover_scan_targets(resolved)
|
|
548
|
+
if discovery.scope == "repository":
|
|
549
|
+
if requested_ecosystem == Ecosystem.CODEX:
|
|
550
|
+
return _scan_repository(resolved, scan_options)
|
|
551
|
+
if requested_ecosystem is None:
|
|
552
|
+
detected_candidates = detect_packages(resolved)
|
|
553
|
+
if not detected_candidates or all(
|
|
554
|
+
candidate.ecosystem == Ecosystem.CODEX for candidate in detected_candidates
|
|
555
|
+
):
|
|
556
|
+
return _scan_repository(resolved, scan_options)
|
|
557
|
+
return _scan_non_repository_target(resolved, scan_options)
|