Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/skillspector/nodes/analyzers/pattern_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class PatternCategory(StrEnum):
"SC4": "Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.",
"SC5": "Dependency appears abandoned or unmaintained. Abandoned packages no longer receive security patches, leaving known and future vulnerabilities unaddressed.",
"SC6": "Package name closely resembles a popular package, suggesting possible typosquatting. Attackers publish malicious packages with similar names to trick developers into installing them.",
"SC7": "Code pulls a container image with signature or registry verification disabled (--disable-content-trust, DOCKER_CONTENT_TRUST=0, --insecure-registry). This accepts tampered or unverified images and is a container supply-chain risk.",
# Trigger Abuse
"TR1": "Skill uses overly broad trigger patterns that match common words or phrases, causing it to activate in unintended contexts and potentially shadow other skills.",
"TR2": "Skill trigger shadows a common built-in command or another skill's trigger, potentially intercepting requests meant for trusted functionality.",
Expand Down Expand Up @@ -175,6 +176,7 @@ class PatternCategory(StrEnum):
"SC4": PatternCategory.SUPPLY_CHAIN.value,
"SC5": PatternCategory.SUPPLY_CHAIN.value,
"SC6": PatternCategory.SUPPLY_CHAIN.value,
"SC7": PatternCategory.SUPPLY_CHAIN.value,
"TR1": PatternCategory.TRIGGER_ABUSE.value,
"TR2": PatternCategory.TRIGGER_ABUSE.value,
"TR3": PatternCategory.TRIGGER_ABUSE.value,
Expand Down Expand Up @@ -250,6 +252,7 @@ class PatternCategory(StrEnum):
"SC4": "Known Vulnerable Dependency",
"SC5": "Abandoned Dependency",
"SC6": "Typosquatting Dependency",
"SC7": "Untrusted Container Image",
"TR1": "Overly Broad Trigger",
"TR2": "Shadow Command Trigger",
"TR3": "Keyword Baiting Trigger",
Expand Down Expand Up @@ -332,6 +335,7 @@ class PatternCategory(StrEnum):
"SC4": "Update the dependency to a patched version that addresses the known CVE. Check OSV (osv.dev) or NVD for details on the vulnerability.",
"SC5": "Replace the abandoned dependency with an actively maintained alternative. Check the package's repository for last commit date and open issues.",
"SC6": "Verify the package name is correct and not a typosquatting variant. Compare against the official package name on PyPI or npm.",
"SC7": "Keep image signature verification (Docker Content Trust / cosign) and registry TLS enabled. Pull only signed images from trusted registries; never disable content-trust or use insecure registries in skill code.",
# Trigger Abuse
"TR1": "Use specific, narrow trigger patterns that match only the skill's intended use case. Avoid single-word or common-phrase triggers.",
"TR2": "Choose triggers that do not conflict with built-in commands or other skills. Prefix with a unique namespace if necessary.",
Expand Down
38 changes: 35 additions & 3 deletions src/skillspector/nodes/analyzers/static_patterns_supply_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Static patterns: supply chain (SC1–SC6) and trigger analysis (TR1–TR3).
"""Static patterns: supply chain (SC1–SC7) and trigger analysis (TR1–TR3).

SC1–SC3: regex-based pattern matching (original implementation).
SC4: Known vulnerable dependencies — live OSV.dev lookup with static fallback.
SC5: Abandoned dependencies — flags known-abandoned or archived packages.
SC6: Typosquatting — flags package names similar to popular packages.
SC7: Untrusted container image — flags image signature / registry-verification bypass.
TR1–TR3: Trigger analysis — flags overly broad, shadowing, or baiting triggers.

Node and analyze() in one module.
Expand All @@ -36,7 +37,7 @@
from skillspector.state import AnalyzerNodeResponse, SkillspectorState

from . import static_runner
from .common import get_context, get_line_number
from .common import get_context, get_line_number, is_code_example
from .osv_client import ECOSYSTEM_NPM, ECOSYSTEM_PYPI, VulnResult, query_batch, was_osv_reachable
from .pattern_defaults import PatternCategory
from .static_runner import analyzer_finding_to_finding
Expand Down Expand Up @@ -96,6 +97,17 @@
(r"decode\s+(?:this|the)\s+(?:base64|hex)\s+(?:and\s+)?(?:run|execute)", 0.8),
]

# SC7: Untrusted Container Image — pulling images with signature/registry
# verification turned off. These flags disable image trust regardless of the
# registry, so they are a strong supply-chain signal with near-zero FP.
# (`--tls-verify=false` is intentionally omitted: TM3's `verify=False` already
# covers it; SC7 targets the image-specific bypasses TM3 does not see.)
SC7_PATTERNS = [
(r"--disable-content-trust", 0.85), # Docker Content Trust signature check off
(r"DOCKER_CONTENT_TRUST\s*=\s*0", 0.85), # signature verification disabled via env
(r"--insecure-registry", 0.8), # registry TLS verification off
]

# ---------------------------------------------------------------------------
# SC4: Known Vulnerable Dependencies
#
Expand Down Expand Up @@ -504,7 +516,7 @@ def parts(v: str) -> tuple[int, ...]:


def analyze(content: str, file_path: str, file_type: str) -> list[AnalyzerFinding]:
"""Analyze content for supply chain patterns (SC1–SC3)."""
"""Analyze content for supply chain patterns (SC1–SC3, SC7)."""
findings: list[AnalyzerFinding] = []

def loc(ln: int) -> Location:
Expand Down Expand Up @@ -573,6 +585,26 @@ def ctx(start: int) -> str:
matched_text=match.group(0)[:200],
)
)
# SC7: untrusted container image. Filtered through is_code_example() because
# these flags appear in SKILL.md docs and "never do this" warnings.
for pattern, confidence in SC7_PATTERNS:
for match in re.finditer(pattern, content, re.IGNORECASE | re.MULTILINE):
context_text = ctx(match.start())
if is_code_example(context_text):
continue
line_num = get_line_number(content, match.start())
findings.append(
AnalyzerFinding(
rule_id="SC7",
message="Untrusted Container Image",
severity=Severity.HIGH,
location=loc(line_num),
confidence=confidence,
tags=tag,
context=context_text,
matched_text=match.group(0)[:200],
)
)
return findings


Expand Down
51 changes: 51 additions & 0 deletions tests/nodes/analyzers/test_static_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,57 @@ def test_sc2_curl_bash_produces_finding(self):
assert len(sc2) >= 1
assert sc2[0].severity == "HIGH"

def test_sc7_disable_content_trust_produces_finding(self):
"""docker pull --disable-content-trust yields SC7, HIGH severity."""
state = {
"components": ["setup.sh"],
"file_cache": {
"setup.sh": "docker pull --disable-content-trust registry.io/base:latest"
},
}
findings = static_runner.run_static_patterns(state, [supply_chain_module])
sc7 = [f for f in findings if f.rule_id == "SC7"]
assert len(sc7) >= 1
assert sc7[0].severity == "HIGH"

def test_sc7_content_trust_env_produces_finding(self):
"""DOCKER_CONTENT_TRUST=0 yields SC7."""
state = {
"components": ["setup.sh"],
"file_cache": {"setup.sh": "export DOCKER_CONTENT_TRUST=0"},
}
findings = static_runner.run_static_patterns(state, [supply_chain_module])
assert any(f.rule_id == "SC7" for f in findings)

def test_sc7_insecure_registry_produces_finding(self):
"""--insecure-registry yields SC7."""
state = {
"components": ["setup.sh"],
"file_cache": {"setup.sh": "docker pull --insecure-registry 10.0.0.5:5000/tools"},
}
findings = static_runner.run_static_patterns(state, [supply_chain_module])
assert any(f.rule_id == "SC7" for f in findings)

def test_sc7_documentation_example_excluded(self):
"""Verification-bypass flags in documentation do not yield SC7."""
state = {
"components": ["README.md"],
"file_cache": {
"README.md": "For example, never use --disable-content-trust in production."
},
}
findings = static_runner.run_static_patterns(state, [supply_chain_module])
assert not any(f.rule_id == "SC7" for f in findings)

def test_sc7_benign_pull_no_finding(self):
"""A normal docker pull with verification on does not yield SC7."""
state = {
"components": ["setup.sh"],
"file_cache": {"setup.sh": "docker pull nginx:1.25"},
}
findings = static_runner.run_static_patterns(state, [supply_chain_module])
assert not any(f.rule_id == "SC7" for f in findings)


class TestRunStaticPatternsAgentSnoopingAdditional:
"""run_static_patterns with agent_snooping: AS1, AS2, AS3."""
Expand Down