Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
153b8a3
fix(ingest): inspect unverified OMS bundle contents
yashrajp22 Oct 7, 2026
7d95757
test(ingest): retain normal findings for unsigned OMS fixtures
yashrajp22 Oct 7, 2026
45f5c04
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 7, 2026
2712948
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 8, 2026
88de383
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 8, 2026
14f6a36
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 8, 2026
f0d1d14
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 8, 2026
ac214bf
fix(oms): inspect decoded bundle content without binary false positives
yashrajp22 Oct 9, 2026
52d03b3
fix(oms): preserve readable Unicode in binary projections
yashrajp22 Oct 9, 2026
6674959
test(oms): assert detection independently of risk thresholds
yashrajp22 Oct 9, 2026
24d87bd
test(oms): exercise unknown base64 in SC3 eligible carrier
yashrajp22 Oct 9, 2026
6ecc53d
fix(oms): keep escaped duplicate-key bundles explicitly partial
yashrajp22 Oct 9, 2026
c43480d
style(test): format OMS regression parameters
yashrajp22 Oct 9, 2026
52a3f78
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 9, 2026
871c00e
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 9, 2026
dbf8453
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 9, 2026
f1fbee6
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 9, 2026
09a357f
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 9, 2026
e60a2b2
merge: preserve OMS projections with Python source classification
yashrajp22 Oct 9, 2026
bd96815
merge: preserve security fixes with latest review branch
yashrajp22 Oct 9, 2026
820ac92
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 9, 2026
a2385ee
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 9, 2026
6194430
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 12, 2026
0dfd73c
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 13, 2026
3748ff8
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 13, 2026
26a9ceb
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 13, 2026
1e39b85
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
e826268
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
83c838b
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
5f18791
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
a0156a8
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
71970e0
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
523e08f
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
9078100
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
364f5b3
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
89c05c5
fix: preserve analysis through malformed and alternative OMS bundles
yashrajp22 Oct 14, 2026
964429f
test: make unsupported certificate extension probe explicit
yashrajp22 Oct 14, 2026
82e5562
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
d76b502
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
7cd62bf
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
c993e53
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
84de79c
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
a46e6bf
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 14, 2026
cc0badd
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 15, 2026
17d94b2
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 15, 2026
d95d35b
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 16, 2026
37a27de
Merge branch 'main' into yashraj/scan-unverified-oms-content
github-actions[bot] Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -934,13 +934,15 @@ SkillSpector uses a two-stage detection pipeline:
- High recall (catches most issues)
- Moderate precision (some false positives)

A valid, root-level OpenSSF Model Signing signature (`skill.oms.sig`) is retained in the
component inventory as type `oms_signature`, but excluded from static and LLM content analysis.
OMS bundles necessarily contain long base64-encoded payload, signature, and certificate fields;
generic obfuscated-code checks can otherwise misclassify those fields as hidden executable content.
The recognizer checks the minimal OMS DSSE/in-toto structure; it does not verify the signature,
certificate chain, transparency-log entry, or signer identity. Invalid or unrecognized signature
files are scanned normally.
OpenSSF Model Signing / DSSE bundles remain in content analysis, regardless of
filename. Static and LLM analyzers inspect decoded JSON payloads, readable DER
certificate/signature content, and all unknown wrapper fields. Strictly parsed
binary fields are projected rather than treated as executable base64; canonical
bytes remain available to YARA and fingerprints. Unsupported encodings (including
environments without the optional `cryptography` dependency) retain raw fields
and produce an explicit incomplete-analysis warning. Findings in decoded content
point to the original bundle and label their projection in evidence. None of this
verifies signatures, certificate chains, transparency logs, or signer identity.

### Stage 2: LLM Semantic Analysis (Optional)
- Evaluates context and intent
Expand Down Expand Up @@ -969,7 +971,7 @@ The tool requires outbound HTTPS access to `api.osv.dev` for live vulnerability
SkillSpector is defense-in-depth, not a sandbox. Know what it does and does not do before relying on it:

- **It never executes the scanned skill.** All analysis is static (regex, Python AST, YARA) plus optional LLM evaluation of file *contents* — the skill's code is never run.
- **LLM analysis sends analyzer-eligible file contents to the configured provider.** When LLM analysis is enabled (the default), file contents are sent to the active `SKILLSPECTOR_PROVIDER` endpoint. Recognized OMS signature files are excluded. Use `--no-llm` to keep contents local (static analysis only).
- **LLM analysis sends analyzer-eligible file contents to the configured provider.** When LLM analysis is enabled (the default), file contents are sent to the active `SKILLSPECTOR_PROVIDER` endpoint. DSSE payloads and readable signing metadata are included. Use `--no-llm` to keep contents local (static analysis only).
- **SC4 sends dependency names to OSV.dev.** The supply-chain check queries [OSV.dev](https://osv.dev) with the package names and versions the skill declares, to look up known CVEs. This is fundamental to the check and runs even with `--no-llm`. It sends dependency coordinates (not file contents), requires no API key, and falls back to a bundled list when OSV.dev is unreachable.
- **It does not sandbox the host.** SkillSpector flags risky patterns *before* you install a skill; it does not contain or isolate a skill you choose to install anyway.

Expand Down
2 changes: 1 addition & 1 deletion docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ All targets assume the virtual environment is **already created and activated**.
| `zip_bytes`, `mode` | Optional zip input and scan mode |
| `components` | List of relative file paths in the skill |
| `file_cache` | Map of path → file contents |
| `inspection_ledger` | Structured evidence for files excluded, skipped, or failed during analysis; a recognized OMS signature is recorded as an `oms_signature` scope exclusion. |
| `inspection_ledger` | Structured evidence for files excluded, skipped, or failed during analysis; unsupported DSSE decoding is recorded as an `oms_signature` partial event; successfully projected bundles remain in analysis without a scope exclusion. |
| `ast_cache` | Map of path → AST representation (for future use) |
| `manifest`, `previous_manifest` | Parsed skill metadata (e.g. from SKILL.md) |
| `component_metadata` | List of dicts: path, type, lines, executable, size_bytes (from build_context) |
Expand Down
2 changes: 1 addition & 1 deletion src/skillspector/inspection_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class LedgerReason(StrEnum):
LedgerReason.MANIFEST_ABSENT: ("No compatible manifest was present for this analyzer."),
LedgerReason.NO_APPLICABLE_FILES: ("No files matched this analyzer's applicability contract."),
LedgerReason.OMS_SIGNATURE: (
"Recognized OMS signature metadata is excluded from content analysis."
"DSSE content decoding is incomplete or unsupported; raw fields were retained for inspection. Signature authenticity was not verified."
),
LedgerReason.BASELINE_FILE: (
"The explicitly selected suppression baseline is excluded from content analysis."
Expand Down
38 changes: 38 additions & 0 deletions src/skillspector/nodes/analyzers/static_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
observe_analyzer_findings,
)
from skillspector.nodes.deduplicate import classification_metadata_key
from skillspector.oms import project_oms_content
from skillspector.python_ast import (
MAX_PYTHON_AST_SOURCE_CHARS,
ParsedPythonFile,
Expand Down Expand Up @@ -2310,9 +2311,46 @@ def _scan_all_views_detailed(
started_at: float | None = None,
python_ast: ParsedPythonFile | None = None,
python_source: bool | None = None,
_oms_projected: bool = False,
) -> tuple[list[Finding], LedgerReason | None, dict[str, int | float]]:
"""Scan bounded raw windows and return any limit with observed/limit metrics."""
started_at = time.monotonic() if started_at is None else started_at
runtime_limit = MAX_STATIC_ANALYSIS_SECONDS_PER_ARTIFACT

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe it shouldnt be hardcoded but behind feature flag

if timeout_seconds is not None:
runtime_limit = min(runtime_limit, max(0.0, timeout_seconds))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont get this logic

now = time.monotonic()
if now >= started_at + runtime_limit:
return (
[],
LedgerReason.RUNTIME_LIMIT,
{
"observed_seconds": max(0.0, now - started_at),
"limit_seconds": runtime_limit,
},
)
projection = None if _oms_projected else project_oms_content(content)
if projection is not None and projection.view.name != "raw":
findings, reason, metrics = _scan_all_views_detailed(
path,
projection.view.text,
pattern_modules,
None,
max_findings=max_findings,
timeout_seconds=timeout_seconds,
started_at=started_at,
# This view is derived JSON metadata, not a Python execution surface.
python_source=False,
_oms_projected=True,
)
for finding in findings:
finding.start_line = finding.end_line = 1
finding.start_column = finding.end_column = None
finding.evidence["oms_projection"] = (
"Decoded content in the original DSSE bundle; line 1 identifies the carrier."
)
finding.evidence[_SOURCE_START_EVIDENCE] = projection.view.source_offset(0)
finding.evidence.pop(_SOURCE_END_EVIDENCE, None)
return findings, reason, metrics
ast_modules = [module for module in pattern_modules if _uses_python_ast(module)]
lexical_modules = [module for module in pattern_modules if not _uses_python_ast(module)]
if python_source is None:
Expand Down
64 changes: 44 additions & 20 deletions src/skillspector/nodes/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
is_executable_content,
is_zip_content,
)
from skillspector.oms import project_oms_content
from skillspector.python_ast import (
PythonSourceClassification,
classify_python_source,
Expand Down Expand Up @@ -908,7 +909,7 @@ def _decode_base64_json(value: object) -> dict[str, object] | None:


def _is_valid_oms_signature_bytes(data: bytes) -> bool:
"""Recognize the minimal OMS DSSE/in-toto structure from bounded bytes."""
"""Recognize OMS structure only; this does not verify or trust its contents."""
Comment thread
yashrajp22 marked this conversation as resolved.
try:
if len(data) > MAX_FILE_BYTES:
return False
Expand Down Expand Up @@ -2897,16 +2898,7 @@ def _record_processing_runtime(*, phase: str, path: str, now: float) -> None:
elif signature_valid:
recognized_oms_signature_paths.add(_OMS_SIGNATURE_PATH)
recognized_oms_signatures = frozenset(recognized_oms_signature_paths)
signature_events = [
ledger_event(
outcome=LedgerOutcome.OUT_OF_SCOPE,
record_type=LedgerRecordType.SCOPE_BOUNDARY,
phase="discovery",
path=path,
reason=LedgerReason.OMS_SIGNATURE,
)
for path in sorted(recognized_oms_signatures)
]
signature_events: list[InspectionLedgerEvent] = []
baseline_events = [
ledger_event(
outcome=LedgerOutcome.OUT_OF_SCOPE,
Expand All @@ -2917,12 +2909,6 @@ def _record_processing_runtime(*, phase: str, path: str, now: float) -> None:
)
for path in sorted(selected_baselines)
]
for artifact in artifact_inventory:
if artifact["path"] in recognized_oms_signatures:
artifact["disposition"] = ArtifactDisposition.OUT_OF_SCOPE
artifact["reason"] = LedgerReason.OMS_SIGNATURE.value
llm_file_cache.pop(artifact["path"], None)

primary_path = next(
(path for path in ("SKILL.md", "skill.md") if path in inventoried_components), None
)
Expand Down Expand Up @@ -3090,8 +3076,6 @@ def _record_processing_runtime(*, phase: str, path: str, now: float) -> None:
ordinary_components: list[str] = []
for path in cache_candidates:
ordinary_artifact = inventory_by_path.get(path)
if path in recognized_oms_signatures:
continue
if path in raw_file_cache or (
ordinary_artifact is not None
and ordinary_artifact.get("disposition")
Expand Down Expand Up @@ -3474,7 +3458,7 @@ def mark_excluded_nested_metadata(
]
)
)
for path in [*recognized_containers, *recognized_oms_signatures]:
for path in recognized_containers:
Comment thread
yashrajp22 marked this conversation as resolved.
llm_file_cache.pop(path, None)

postprocessing_events: list[InspectionLedgerEvent] = []
Expand Down Expand Up @@ -3647,6 +3631,46 @@ def _mark_runtime_partial(affected_paths: list[str], first_limited_path: str) ->
)
)

for path, content in local_file_cache.items():
if not content.lstrip().startswith("{"):
continue
projection_started = monotonic()
if projection_started >= processing_deadline:
_record_processing_runtime(
phase="signature_projection", path=path, now=projection_started
)
continue
projection = project_oms_content(content)
if projection is None:
continue
projection_finished = monotonic()
if projection_finished >= processing_deadline:
_record_processing_runtime(
phase="signature_projection", path=path, now=projection_finished
)
continue
artifact = inventory_by_path.get(path)
bounded_provider_view = artifact is not None and (
artifact.get("disposition") == ArtifactDisposition.PARTIAL
and artifact.get("reason")
in {LedgerReason.SIZE_LIMIT.value, LedgerReason.TOTAL_BYTES_LIMIT.value}
)
if path in llm_file_cache and projection.view.name != "raw" and not bounded_provider_view:
llm_file_cache[path] = projection.view.text
if not projection.complete:
signature_events.append(
ledger_event(
outcome=LedgerOutcome.PARTIAL,
record_type=LedgerRecordType.SYSTEM,
phase="discovery",
path=path,
reason=LedgerReason.OMS_SIGNATURE,
)
)
if artifact is not None and artifact["disposition"] == ArtifactDisposition.ANALYZED:
artifact["disposition"] = ArtifactDisposition.PARTIAL
artifact["reason"] = LedgerReason.OMS_SIGNATURE.value

llm_components = sorted(llm_file_cache)
file_cache = dict(llm_file_cache)
manifest_events: list[InspectionLedgerEvent] = []
Expand Down
Loading
Loading