Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/skillspector/nodes/analyzers/mcp_least_privilege.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ def emit(self, finding: Finding) -> None:
},
path=finding.file,
)
finding.pattern = finding.pattern or finding.message
Comment thread
rng1995 marked this conversation as resolved.
finding.finding = (finding.finding or finding.matched_text or finding.message)[:200]
self.findings.append(finding)
self.artifact_findings[finding.file] = artifact_observed

Expand Down
2 changes: 2 additions & 0 deletions src/skillspector/nodes/analyzers/mcp_rug_pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ def emit(self, finding: Finding) -> None:
"limit_findings": MAX_FINDINGS_PER_ANALYZER,
},
)
finding.pattern = finding.pattern or finding.message
Comment thread
rng1995 marked this conversation as resolved.
finding.finding = (finding.finding or finding.matched_text or finding.message)[:200]
self.findings.append(finding)
self.artifact_findings[finding.file] = artifact_observed

Expand Down
2 changes: 2 additions & 0 deletions src/skillspector/nodes/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
# Finding free-text fields that may carry scanned/LLM content.
_SANITIZED_FIELDS = (
"message",
"pattern",
"explanation",
"remediation",
"finding",
Expand Down Expand Up @@ -144,6 +145,7 @@ def clean_evidence(value: object) -> object:
return replace(
finding,
message=clean(finding.message) or "",
pattern=clean(finding.pattern),
explanation=clean(finding.explanation),
remediation=clean(finding.remediation),
finding=clean(finding.finding),
Expand Down
9 changes: 9 additions & 0 deletions tests/nodes/analyzers/test_mcp_rug_pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ def test_rp3_parameter_added(self) -> None:
assert f.confidence == 0.80
assert "added: force" in f.message

def test_rp3_manifest_change_finding_is_short(self) -> None:
state = {
"manifest": {"parameters": [{"name": "p" * 250}]},
"previous_manifest": {"parameters": []},
}
finding = next(f for f in node(state)["findings"] if f.rule_id == "RP3")
assert len(finding.message) > 200
assert finding.finding == finding.message[:200]

def test_rp3_parameter_removed(self) -> None:
"""RP3 is triggered when a parameter is removed."""
state = {
Expand Down
3 changes: 3 additions & 0 deletions tests/nodes/test_report_sanitizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def _dirty_finding() -> Finding:
return Finding(
rule_id="E2",
message="creds \x1b[31mleak\x1b[0m here\x00",
pattern="pattern \x1b[31mleak\x1b[0m here\x00",
severity="HIGH",
confidence=0.9,
file="a/SKILL.md",
Expand All @@ -53,6 +54,7 @@ def test_sanitize_finding_cleans_text_fields_only() -> None:
cleaned = _sanitize_finding(_dirty_finding())
assert "\x1b" not in cleaned.message and "\x00" not in cleaned.message
assert "leak" in cleaned.message and "here" in cleaned.message
assert cleaned.pattern == "pattern leak here"
assert "\x1b" not in (cleaned.remediation or "")
assert "\x07" not in (cleaned.context or "")
# Non-text fields are unchanged.
Expand Down Expand Up @@ -92,6 +94,7 @@ def test_report_redacts_url_credentials_from_every_finding_field(fmt: str, schem
confidence=0.9,
file="setup.sh",
start_line=1,
pattern=url,
finding=url,
explanation=url,
remediation=url,
Expand Down
7 changes: 7 additions & 0 deletions tests/test_mcp_least_privilege.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@

from __future__ import annotations

import json
import re
from pathlib import Path

import yaml

from skillspector.nodes.analyzers import mcp_least_privilege
from skillspector.nodes.report import report

# ---------------------------------------------------------------------------
# Fixture directory path
Expand Down Expand Up @@ -259,6 +261,11 @@ def test_no_permissions_field(self):
assert lp3.severity == "MEDIUM"
assert lp3.confidence >= 0.70
assert lp3.file == "SKILL.md"
issue = json.loads(
report({"filtered_findings": [lp3], "output_format": "json"})["report_body"]
)["issues"][0]
assert issue["pattern"] == lp3.message
assert issue["finding"] == lp3.message


class TestLP3AllowedTools:
Expand Down
27 changes: 27 additions & 0 deletions tests/test_mcp_rug_pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from skillspector.nodes.analyzers.mcp_rug_pull import node
from skillspector.nodes.build_context import build_context
from skillspector.nodes.deduplicate import deduplicate
from skillspector.nodes.report import report
from skillspector.state import SkillspectorState


Expand All @@ -47,6 +48,11 @@ def test_rp1_npx_unpinned():
rp1 = [f for f in result["findings"] if f.rule_id == "RP1"]
assert len(rp1) == 1
assert "npx @scope/mcp-server" in rp1[0].matched_text
issue = json.loads(report({"filtered_findings": rp1, "output_format": "json"})["report_body"])[
"issues"
][0]
assert issue["pattern"] == rp1[0].message
assert issue["finding"] == "npx @scope/mcp-server"


def test_rp1_scans_cached_files_without_a_manifest():
Expand Down Expand Up @@ -114,6 +120,27 @@ def test_rp1_docker_unpinned():
assert len(rp1) >= 1


def test_rp1_docker_credentials_are_redacted_in_reports():
result = node(
_state(
file_cache={
"setup.sh": "docker pull https://deploy:s3cret@registry.example.com/team/image"
}
)
)
rp1 = [f for f in result["findings"] if f.rule_id == "RP1"]
assert len(rp1) == 1

json_body = report({"filtered_findings": rp1, "output_format": "json"})["report_body"]
issue = json.loads(json_body)["issues"][0]
assert "https://***@registry.example.com" in issue["pattern"]
assert "https://***@registry.example.com" in issue["finding"]
sarif_body = report({"filtered_findings": rp1, "output_format": "sarif"})["report_body"]
for body in (json_body, sarif_body):
assert "deploy:s3cret" not in body
assert "s3cret" not in body


def test_rp1_multiple_patterns():
"""Multiple unpinned references produce multiple RP1 findings."""
result = node(
Expand Down
Loading