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,116 @@
|
|
|
1
|
+
"""HCS-style MCP trust scoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .models import CategoryResult
|
|
8
|
+
from .trust_helpers import (
|
|
9
|
+
build_adapter_score,
|
|
10
|
+
build_domain_score,
|
|
11
|
+
category_checks,
|
|
12
|
+
check_percent,
|
|
13
|
+
is_https_url,
|
|
14
|
+
load_mcp_payload,
|
|
15
|
+
)
|
|
16
|
+
from .trust_models import TrustDomainScore
|
|
17
|
+
from .trust_specs import MCP_TRUST_SPEC
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_mcp_domain(plugin_dir: Path, categories: tuple[CategoryResult, ...]) -> TrustDomainScore | None:
|
|
21
|
+
payload_state = load_mcp_payload(plugin_dir)
|
|
22
|
+
if payload_state is None:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
payload = payload_state.payload
|
|
26
|
+
security_checks = category_checks(categories, "Security")
|
|
27
|
+
remotes = payload.get("remotes")
|
|
28
|
+
servers = payload.get("mcpServers")
|
|
29
|
+
remote_entries = remotes if isinstance(remotes, list) else []
|
|
30
|
+
local_servers = servers if isinstance(servers, dict) else {}
|
|
31
|
+
has_named_surfaces = bool(remote_entries) or bool(local_servers)
|
|
32
|
+
secure_remote_urls = (
|
|
33
|
+
all(isinstance(entry, dict) and is_https_url(str(entry.get("url", ""))) for entry in remote_entries)
|
|
34
|
+
if remote_entries
|
|
35
|
+
else True
|
|
36
|
+
)
|
|
37
|
+
local_commands_valid = (
|
|
38
|
+
all(
|
|
39
|
+
isinstance(config, dict)
|
|
40
|
+
and isinstance(config.get("command"), str)
|
|
41
|
+
and bool(config.get("command"))
|
|
42
|
+
and (
|
|
43
|
+
"args" not in config
|
|
44
|
+
or (
|
|
45
|
+
isinstance(config.get("args"), list) and all(isinstance(value, str) for value in config.get("args"))
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
for config in local_servers.values()
|
|
49
|
+
)
|
|
50
|
+
if local_servers
|
|
51
|
+
else True
|
|
52
|
+
)
|
|
53
|
+
config_shape = payload_state.parse_valid and (
|
|
54
|
+
(remotes is None or isinstance(remotes, list)) and (servers is None or isinstance(servers, dict))
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
spec_by_id = {adapter.adapter_id: adapter for adapter in MCP_TRUST_SPEC.adapters}
|
|
58
|
+
adapters = (
|
|
59
|
+
build_adapter_score(
|
|
60
|
+
spec_by_id["verification.config-integrity"],
|
|
61
|
+
component_scores={"score": 100.0} if payload_state.parse_valid else None,
|
|
62
|
+
rationales={
|
|
63
|
+
"score": (
|
|
64
|
+
"The .mcp.json file parsed successfully."
|
|
65
|
+
if payload_state.parse_valid
|
|
66
|
+
else "The .mcp.json file did not parse, so config-integrity remains 0."
|
|
67
|
+
)
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
build_adapter_score(
|
|
71
|
+
spec_by_id["verification.execution-safety"],
|
|
72
|
+
component_scores={"score": check_percent(security_checks, "No dangerous MCP commands")},
|
|
73
|
+
rationales={"score": "Execution safety follows the scanner's dangerous-command check."},
|
|
74
|
+
),
|
|
75
|
+
build_adapter_score(
|
|
76
|
+
spec_by_id["verification.transport-security"],
|
|
77
|
+
component_scores={"score": check_percent(security_checks, "MCP remote transports are hardened")},
|
|
78
|
+
rationales={"score": "Transport security follows the scanner's hardened-remote check."},
|
|
79
|
+
),
|
|
80
|
+
build_adapter_score(
|
|
81
|
+
spec_by_id["metadata.server-naming"],
|
|
82
|
+
component_scores={"score": 100.0} if has_named_surfaces else None,
|
|
83
|
+
rationales={
|
|
84
|
+
"score": (
|
|
85
|
+
"At least one MCP surface is explicitly named."
|
|
86
|
+
if has_named_surfaces
|
|
87
|
+
else "No local or remote MCP surfaces are declared."
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
),
|
|
91
|
+
build_adapter_score(
|
|
92
|
+
spec_by_id["metadata.command-or-endpoint"],
|
|
93
|
+
component_scores=(
|
|
94
|
+
{"score": 100.0} if has_named_surfaces and secure_remote_urls and local_commands_valid else None
|
|
95
|
+
),
|
|
96
|
+
rationales={
|
|
97
|
+
"score": (
|
|
98
|
+
"Every MCP surface declares a concrete command or HTTPS endpoint."
|
|
99
|
+
if has_named_surfaces and secure_remote_urls and local_commands_valid
|
|
100
|
+
else "At least one MCP surface is missing a valid command or secure endpoint."
|
|
101
|
+
)
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
build_adapter_score(
|
|
105
|
+
spec_by_id["metadata.config-shape"],
|
|
106
|
+
component_scores={"score": 100.0} if config_shape else None,
|
|
107
|
+
rationales={
|
|
108
|
+
"score": (
|
|
109
|
+
"The top-level MCP config containers match the expected shape."
|
|
110
|
+
if config_shape
|
|
111
|
+
else "The MCP config containers do not match the expected shape."
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
return build_domain_score(domain="mcp", spec=MCP_TRUST_SPEC, adapters=adapters)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Trust provenance data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
ContributionMode = Literal["conditional", "universal", "scoped"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class TrustComponentScore:
|
|
13
|
+
"""One scored trust component inside an adapter."""
|
|
14
|
+
|
|
15
|
+
key: str
|
|
16
|
+
score: float
|
|
17
|
+
rationale: str
|
|
18
|
+
evidence: tuple[str, ...] = ()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class TrustAdapterScore:
|
|
23
|
+
"""Weighted adapter score inside a trust domain."""
|
|
24
|
+
|
|
25
|
+
adapter_id: str
|
|
26
|
+
label: str
|
|
27
|
+
weight: float
|
|
28
|
+
contribution_mode: ContributionMode
|
|
29
|
+
applicable: bool
|
|
30
|
+
emitted: bool
|
|
31
|
+
included_in_denominator: bool
|
|
32
|
+
score: float
|
|
33
|
+
components: tuple[TrustComponentScore, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class TrustDomainScore:
|
|
38
|
+
"""One trust domain such as plugin, skills, or MCP."""
|
|
39
|
+
|
|
40
|
+
domain: str
|
|
41
|
+
label: str
|
|
42
|
+
spec_id: str
|
|
43
|
+
spec_version: str
|
|
44
|
+
spec_path: str
|
|
45
|
+
derived_from: tuple[str, ...]
|
|
46
|
+
profile_id: str
|
|
47
|
+
profile_version: str
|
|
48
|
+
score: float
|
|
49
|
+
adapters: tuple[TrustAdapterScore, ...]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class TrustReport:
|
|
54
|
+
"""Overall trust report emitted alongside scan results."""
|
|
55
|
+
|
|
56
|
+
total: float
|
|
57
|
+
include_external: bool
|
|
58
|
+
computed_at: str
|
|
59
|
+
domains: tuple[TrustDomainScore, ...] = ()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class TrustAdapterSpec:
|
|
64
|
+
"""Definition of a trust adapter and its components."""
|
|
65
|
+
|
|
66
|
+
adapter_id: str
|
|
67
|
+
label: str
|
|
68
|
+
weight: float
|
|
69
|
+
component_keys: tuple[str, ...]
|
|
70
|
+
contribution_mode: ContributionMode = "conditional"
|
|
71
|
+
default_component_key: str = "score"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class TrustSpecDefinition:
|
|
76
|
+
"""Definition of a trust scoring specification."""
|
|
77
|
+
|
|
78
|
+
spec_id: str
|
|
79
|
+
version: str
|
|
80
|
+
label: str
|
|
81
|
+
spec_path: str
|
|
82
|
+
derived_from: tuple[str, ...]
|
|
83
|
+
profile_id: str
|
|
84
|
+
profile_version: str
|
|
85
|
+
adapters: tuple[TrustAdapterSpec, ...] = field(default_factory=tuple)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""HCS-style top-level plugin trust scoring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .checks.manifest import load_manifest
|
|
8
|
+
from .models import CategoryResult
|
|
9
|
+
from .trust_helpers import build_adapter_score, build_domain_score, category_checks, check_percent, is_https_url
|
|
10
|
+
from .trust_models import TrustDomainScore
|
|
11
|
+
from .trust_specs import PLUGIN_TRUST_SPEC
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_plugin_domain(plugin_dir: Path, categories: tuple[CategoryResult, ...]) -> TrustDomainScore:
|
|
15
|
+
manifest = load_manifest(plugin_dir)
|
|
16
|
+
manifest_checks = category_checks(categories, "Manifest Validation")
|
|
17
|
+
security_checks = category_checks(categories, "Security")
|
|
18
|
+
operational_checks = category_checks(categories, "Operational Security")
|
|
19
|
+
best_checks = category_checks(categories, "Best Practices")
|
|
20
|
+
marketplace_checks = category_checks(categories, "Marketplace")
|
|
21
|
+
interface = (
|
|
22
|
+
manifest.get("interface") if isinstance(manifest, dict) and isinstance(manifest.get("interface"), dict) else {}
|
|
23
|
+
)
|
|
24
|
+
interface_declared = isinstance(manifest, dict) and "interface" in manifest
|
|
25
|
+
author = manifest.get("author") if isinstance(manifest, dict) and isinstance(manifest.get("author"), dict) else {}
|
|
26
|
+
has_author = isinstance(author.get("name"), str) and bool(author.get("name"))
|
|
27
|
+
has_homepage = is_https_url(manifest.get("homepage") if isinstance(manifest, dict) else None)
|
|
28
|
+
has_repository = is_https_url(manifest.get("repository") if isinstance(manifest, dict) else None)
|
|
29
|
+
keywords = manifest.get("keywords") if isinstance(manifest, dict) else None
|
|
30
|
+
keyword_count = len(keywords) if isinstance(keywords, list) else 0
|
|
31
|
+
has_interface_category = isinstance(interface.get("category"), str) and bool(interface.get("category"))
|
|
32
|
+
discoverability = (
|
|
33
|
+
100.0
|
|
34
|
+
if has_interface_category and keyword_count >= 3
|
|
35
|
+
else 75.0
|
|
36
|
+
if has_interface_category or keyword_count >= 1
|
|
37
|
+
else 0.0
|
|
38
|
+
)
|
|
39
|
+
provenance = (
|
|
40
|
+
100.0
|
|
41
|
+
if has_author and has_homepage and has_repository
|
|
42
|
+
else 70.0
|
|
43
|
+
if has_author and (has_homepage or has_repository)
|
|
44
|
+
else 40.0
|
|
45
|
+
if has_author or has_repository or has_homepage
|
|
46
|
+
else 0.0
|
|
47
|
+
)
|
|
48
|
+
has_marketplace_surface = (plugin_dir / "marketplace.json").exists()
|
|
49
|
+
has_mcp_surface = (plugin_dir / ".mcp.json").exists()
|
|
50
|
+
|
|
51
|
+
spec_by_id = {adapter.adapter_id: adapter for adapter in PLUGIN_TRUST_SPEC.adapters}
|
|
52
|
+
adapters = (
|
|
53
|
+
build_adapter_score(
|
|
54
|
+
spec_by_id["verification.manifest-integrity"],
|
|
55
|
+
component_scores={
|
|
56
|
+
"score": (
|
|
57
|
+
check_percent(manifest_checks, "plugin.json exists") * 0.20
|
|
58
|
+
+ check_percent(manifest_checks, "Valid JSON") * 0.20
|
|
59
|
+
+ check_percent(manifest_checks, "Required fields present") * 0.35
|
|
60
|
+
+ check_percent(manifest_checks, "Version follows semver") * 0.25
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
rationales={
|
|
64
|
+
"score": "Manifest integrity blends existence, JSON validity, required fields, and semver checks."
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
build_adapter_score(
|
|
68
|
+
spec_by_id["verification.interface-integrity"],
|
|
69
|
+
component_scores={
|
|
70
|
+
"score": (
|
|
71
|
+
check_percent(manifest_checks, "Interface metadata complete if declared") * 0.60
|
|
72
|
+
+ check_percent(manifest_checks, "Interface links and assets valid if declared") * 0.40
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
if interface_declared
|
|
76
|
+
else None,
|
|
77
|
+
rationales={"score": "Interface integrity applies when the plugin declares an install surface."},
|
|
78
|
+
applicable=interface_declared,
|
|
79
|
+
),
|
|
80
|
+
build_adapter_score(
|
|
81
|
+
spec_by_id["verification.path-safety"],
|
|
82
|
+
component_scores={"score": check_percent(manifest_checks, "Declared paths are safe")},
|
|
83
|
+
rationales={"score": "Path safety uses the scanner's declared-path safety check."},
|
|
84
|
+
),
|
|
85
|
+
build_adapter_score(
|
|
86
|
+
spec_by_id["verification.marketplace-alignment"],
|
|
87
|
+
component_scores={
|
|
88
|
+
"score": (
|
|
89
|
+
check_percent(marketplace_checks, "marketplace.json valid") * 0.35
|
|
90
|
+
+ check_percent(marketplace_checks, "Policy fields present") * 0.35
|
|
91
|
+
+ check_percent(marketplace_checks, "Marketplace sources are safe") * 0.30
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
if has_marketplace_surface
|
|
95
|
+
else None,
|
|
96
|
+
rationales={
|
|
97
|
+
"score": "Marketplace alignment applies when repository-scoped marketplace metadata is present."
|
|
98
|
+
},
|
|
99
|
+
applicable=has_marketplace_surface,
|
|
100
|
+
),
|
|
101
|
+
build_adapter_score(
|
|
102
|
+
spec_by_id["security.disclosure"],
|
|
103
|
+
component_scores={"score": check_percent(security_checks, "SECURITY.md found")},
|
|
104
|
+
rationales={"score": "Disclosure is one explicit signal, not a proxy for the entire security posture."},
|
|
105
|
+
),
|
|
106
|
+
build_adapter_score(
|
|
107
|
+
spec_by_id["security.license"],
|
|
108
|
+
component_scores={"score": check_percent(security_checks, "LICENSE found")},
|
|
109
|
+
rationales={"score": "License clarity remains a separate scored signal."},
|
|
110
|
+
),
|
|
111
|
+
build_adapter_score(
|
|
112
|
+
spec_by_id["security.secret-hygiene"],
|
|
113
|
+
component_scores={"score": check_percent(security_checks, "No hardcoded secrets")},
|
|
114
|
+
rationales={"score": "Secret hygiene uses the scanner's hardcoded-secret detection."},
|
|
115
|
+
),
|
|
116
|
+
build_adapter_score(
|
|
117
|
+
spec_by_id["security.mcp-safety"],
|
|
118
|
+
component_scores={
|
|
119
|
+
"score": (
|
|
120
|
+
check_percent(security_checks, "No dangerous MCP commands") * 0.50
|
|
121
|
+
+ check_percent(security_checks, "MCP remote transports are hardened") * 0.50
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
if has_mcp_surface
|
|
125
|
+
else None,
|
|
126
|
+
rationales={"score": "MCP safety applies only when the plugin declares an MCP surface."},
|
|
127
|
+
applicable=has_mcp_surface,
|
|
128
|
+
),
|
|
129
|
+
build_adapter_score(
|
|
130
|
+
spec_by_id["security.approval-hygiene"],
|
|
131
|
+
component_scores={"score": check_percent(security_checks, "No approval bypass defaults")},
|
|
132
|
+
rationales={"score": "Approval hygiene checks for bypass-style defaults."},
|
|
133
|
+
),
|
|
134
|
+
build_adapter_score(
|
|
135
|
+
spec_by_id["metadata.documentation"],
|
|
136
|
+
component_scores={"score": check_percent(best_checks, "README.md found")},
|
|
137
|
+
rationales={"score": "Documentation reflects README coverage for operators and maintainers."},
|
|
138
|
+
),
|
|
139
|
+
build_adapter_score(
|
|
140
|
+
spec_by_id["metadata.manifest-metadata"],
|
|
141
|
+
component_scores={"score": check_percent(manifest_checks, "Recommended metadata present")},
|
|
142
|
+
rationales={"score": "Manifest metadata tracks the scanner's recommended-metadata check."},
|
|
143
|
+
),
|
|
144
|
+
build_adapter_score(
|
|
145
|
+
spec_by_id["metadata.discoverability"],
|
|
146
|
+
component_scores={"score": discoverability},
|
|
147
|
+
rationales={"score": "Discoverability uses category plus keyword coverage."},
|
|
148
|
+
),
|
|
149
|
+
build_adapter_score(
|
|
150
|
+
spec_by_id["metadata.provenance"],
|
|
151
|
+
component_scores={"score": provenance},
|
|
152
|
+
rationales={"score": "Provenance reflects author, homepage, and repository metadata coverage."},
|
|
153
|
+
),
|
|
154
|
+
build_adapter_score(
|
|
155
|
+
spec_by_id["operations.action-pinning"],
|
|
156
|
+
component_scores={"score": check_percent(operational_checks, "Third-party GitHub Actions pinned to SHAs")},
|
|
157
|
+
rationales={"score": "Action pinning uses the scanner's immutable-action check."},
|
|
158
|
+
),
|
|
159
|
+
build_adapter_score(
|
|
160
|
+
spec_by_id["operations.permission-scope"],
|
|
161
|
+
component_scores={"score": check_percent(operational_checks, "No write-all GitHub Actions permissions")},
|
|
162
|
+
rationales={"score": "Permission scope uses the least-privilege workflow check."},
|
|
163
|
+
),
|
|
164
|
+
build_adapter_score(
|
|
165
|
+
spec_by_id["operations.untrusted-checkout"],
|
|
166
|
+
component_scores={"score": check_percent(operational_checks, "No privileged untrusted checkout patterns")},
|
|
167
|
+
rationales={"score": "Untrusted-checkout protection uses the scanner's privileged-workflow check."},
|
|
168
|
+
),
|
|
169
|
+
build_adapter_score(
|
|
170
|
+
spec_by_id["operations.update-automation"],
|
|
171
|
+
component_scores={
|
|
172
|
+
"score": (
|
|
173
|
+
check_percent(operational_checks, "Dependabot configured for automation surfaces") * 0.50
|
|
174
|
+
+ check_percent(operational_checks, "Dependency manifests have lockfiles") * 0.50
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
rationales={"score": "Update automation combines Dependabot coverage and lockfile hygiene."},
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
return build_domain_score(domain="plugin", spec=PLUGIN_TRUST_SPEC, adapters=adapters)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Trust scoring and provenance formatting for scan results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .checks.skill_security import SkillSecurityContext
|
|
9
|
+
from .models import CategoryResult
|
|
10
|
+
from .trust_domain_scoring import build_mcp_domain, build_plugin_domain, build_skill_domain
|
|
11
|
+
from .trust_helpers import normalize_report_total, round_trust_score
|
|
12
|
+
from .trust_models import TrustReport
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_plugin_trust_report(
|
|
16
|
+
plugin_dir: Path,
|
|
17
|
+
categories: tuple[CategoryResult, ...],
|
|
18
|
+
skill_security_context: SkillSecurityContext,
|
|
19
|
+
) -> TrustReport:
|
|
20
|
+
computed_at = datetime.now(timezone.utc).isoformat()
|
|
21
|
+
domains = [
|
|
22
|
+
domain
|
|
23
|
+
for domain in (
|
|
24
|
+
build_plugin_domain(plugin_dir, categories),
|
|
25
|
+
build_skill_domain(plugin_dir, skill_security_context),
|
|
26
|
+
build_mcp_domain(plugin_dir, categories),
|
|
27
|
+
)
|
|
28
|
+
if domain is not None
|
|
29
|
+
]
|
|
30
|
+
scores = tuple(domain.score for domain in domains)
|
|
31
|
+
return TrustReport(
|
|
32
|
+
total=normalize_report_total(scores),
|
|
33
|
+
include_external=False,
|
|
34
|
+
computed_at=computed_at,
|
|
35
|
+
domains=tuple(domains),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_repository_trust_report(plugin_reports: tuple[TrustReport, ...]) -> TrustReport:
|
|
40
|
+
if not plugin_reports:
|
|
41
|
+
return TrustReport(
|
|
42
|
+
total=0.0,
|
|
43
|
+
include_external=False,
|
|
44
|
+
computed_at=datetime.now(timezone.utc).isoformat(),
|
|
45
|
+
domains=(),
|
|
46
|
+
)
|
|
47
|
+
return TrustReport(
|
|
48
|
+
total=round_trust_score(min(report.total for report in plugin_reports)),
|
|
49
|
+
include_external=False,
|
|
50
|
+
computed_at=max(report.computed_at for report in plugin_reports),
|
|
51
|
+
domains=(),
|
|
52
|
+
)
|