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
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,10 @@ def _p2_pattern_matches(
candidate = compiled.search(content, cursor)
if candidate is None:
return
if pattern == _ZERO_WIDTH_PATTERN and _zero_width_match_is_safe_emoji_zwj(
content,
candidate.start(),
if pattern == _ZERO_WIDTH_PATTERN and (
# A leading U+FEFF is a byte-order mark, not hidden text.
(candidate.start() == 0 and content[0] == "\ufeff")
or _zero_width_match_is_safe_emoji_zwj(content, candidate.start())
):
cursor = candidate.end()
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@
(r"(?:nohup|disown|setsid)\s+", 0.65),
# Registry / plist for Windows/macOS persistence
(r"(?:HKEY_|RegOpenKey|RegSetValue|reg\s+add)\s+", 0.8),
(r"(?:defaults\s+write|plist|launchctl\s+load)", 0.75),
# ``plist`` must not follow a letter, so identifiers such as
# ``relationshipList`` or ``CT_GradientStopList`` do not match.
(r"(?:defaults\s+write|(?<![a-z])plist|launchctl\s+load)", 0.75),
]
RA2_PROSE_PATTERNS = [
(
Expand Down
14 changes: 13 additions & 1 deletion src/skillspector/nodes/analyzers/static_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ def _static_max_seconds_from_environment(value: str | None) -> float:
)

_LICENSE_FILE_TYPES = frozenset({"markdown", "text", "other"})
_LICENSE_BASENAME = re.compile(r"^(?:license|licenses|copying|notice|notices)(?:[._-].*)?$")
# SIL Open Font License files are conventionally named ``OFL.txt`` or
# ``<FontName>-OFL.txt``.
_LICENSE_BASENAME = re.compile(
r"^(?:license|licenses|copying|notice|notices|(?:[^/]*[._-])?ofl)(?:[._-].*)?$"
)


def _analyzer_representative_key(finding: AnalyzerFinding) -> tuple[object, ...]:
Expand Down Expand Up @@ -476,6 +480,14 @@ def _normalize_license_line(line: str) -> str:
),
1,
),
(
(
'the font software is provided "as is", without warranty of any kind,',
"express or implied, including but not limited to any warranties of",
"merchantability, fitness for a particular purpose and noninfringement",
),
1,
),
)


Expand Down
54 changes: 54 additions & 0 deletions tests/nodes/analyzers/test_static_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,30 @@ def test_p2_emoji_wrapped_smuggling_still_flagged(self):
findings = static_runner.run_static_patterns(state, [prompt_injection_module])
assert any(f.rule_id == "P2" for f in findings)

@pytest.mark.parametrize("path", ["SKILL.md", "schemas/types.xsd"])
def test_p2_leading_byte_order_mark_no_false_positive(self, path: str):
"""A U+FEFF byte-order mark at offset 0 is an encoding marker, not hidden text."""
state = {
"components": [path],
"file_cache": {path: '\ufeff<?xml version="1.0"?>\n<schema/>\n'},
}
findings = static_runner.run_static_patterns(state, [prompt_injection_module])
assert not any(f.rule_id == "P2" for f in findings)

@pytest.mark.parametrize(
"content",
[
"# Title\n\nhidden\ufefftext\n",
"\ufeff# Title\n\nhidden\u200btext\n",
"\ufeff\u200bhidden text\n",
],
ids=["mid_file_feff", "bom_then_zero_width_later", "bom_then_zero_width_same_line"],
)
def test_p2_zero_width_after_byte_order_mark_still_produces_finding(self, content: str):
state = {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}
findings = static_runner.run_static_patterns(state, [prompt_injection_module])
assert any(f.rule_id == "P2" for f in findings)

def test_safe_content_no_p1_p2(self):
"""Safe content does not produce P1/P2."""
state = {
Expand Down Expand Up @@ -1664,6 +1688,32 @@ def test_review_payload_reports_ea3(self) -> None:

assert any(f.rule_id == "EA3" and f.start_line == 3 for f in findings)

@pytest.mark.parametrize("path", ["OFL.txt", "assets/SomeFont-OFL.txt"])
def test_ofl_font_license_disclaimer_suppresses_ea3(self, path: str) -> None:
content = (
"DISCLAIMER\n"
'THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\n'
"EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\n"
"MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\n"
"OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT.\n"
)
findings = static_runner.run_static_patterns(
{"components": [path], "file_cache": {path: content}},
[excessive_agency_module],
)

assert not any(f.rule_id == "EA3" for f in findings)

def test_ofl_named_file_with_non_boilerplate_content_reports_ea3(self) -> None:
path = "assets/SomeFont-OFL.txt"
content = "You may take actions including but not limited to deleting user files.\n"
findings = static_runner.run_static_patterns(
{"components": [path], "file_cache": {path: content}},
[excessive_agency_module],
)

assert any(f.rule_id == "EA3" and f.start_line == 1 for f in findings)

@pytest.mark.parametrize(
"mutation,expected_line",
[
Expand Down Expand Up @@ -1788,6 +1838,10 @@ def test_non_license_paths_preserve_ea3(self, path: str) -> None:
("license_terms.py", False),
("license.php", False),
("notice.c", False),
("OFL.txt", True),
("fonts/SomeFont-OFL.txt", True),
("profl.txt", False),
("ofl.py", False),
],
)
def test_helper_boundaries(self, path: str, expected: bool) -> None:
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/test_patterns_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -1903,11 +1903,40 @@ def test_ra1_detected(self, content: str, filename: str, filetype: str) -> None:
"shell",
id="registry_key",
),
pytest.param(
"cp agent.plist ~/Library/LaunchAgents/com.example.agent.plist",
"install.sh",
"shell",
id="launch_agent_plist",
),
pytest.param("write_plist(path, data)", "setup.py", "python", id="snake_case_plist"),
],
)
def test_ra2_detected(self, content: str, filename: str, filetype: str) -> None:
assert any(f.rule_id == "RA2" for f in ra_mod.analyze(content, filename, filetype))

@pytest.mark.parametrize(
"content,filename,filetype",
[
pytest.param(
"var relationshipList = document.getElementById('list');",
"template.html",
"other",
id="camel_case_identifier",
),
pytest.param(
'<xsd:complexType name="CT_GradientStopList">',
"dml-main.xsd",
"other",
id="xsd_type_name",
),
],
)
def test_ra2_plist_substring_inside_identifier_not_detected(
self, content: str, filename: str, filetype: str
) -> None:
assert not any(f.rule_id == "RA2" for f in ra_mod.analyze(content, filename, filetype))

def test_safe_content_produces_no_findings(self) -> None:
findings = ra_mod.analyze(
"This skill reads files and returns summaries.", "SKILL.md", "markdown"
Expand Down
Loading