fix(skills-guard): handle inline-comment and docstring false positives for os.environ

The original ^(?!\s*#) prefix only skipped full-line comments starting
with '#'. An inline comment like:
  cfg = environ.get('HOME')  # os.environ available
still triggered python_os_environ because the regex matched the code part
before the '#'.

Two complementary fixes:
1. Replace ^(?!\s*#) with ^[^#\n]* in the regex — this rejects any line
   where a '#' comment marker appears anywhere before os.environ.
2. Add _compute_docstring_lines() — a state machine that pre-computes
   lines inside triple-quoted strings (docstrings) and skips them during
   pattern matching. Also handles single-line self-contained docstrings.

6 new regression tests covering: inline comments, multi-line docstrings,
single-line docstrings, full-line comments, and a verification that real
bare dict(os.environ) code still triggers. All 85 tests pass.
This commit is contained in:
Alli
2026-07-26 13:08:02 +00:00
committed by Teknium
parent 42e6149451
commit 54909d41b4
2 changed files with 94 additions and 4 deletions

View File

@@ -415,6 +415,52 @@ class TestFalsePositiveReductions:
assert sec
assert all(fi.severity == "medium" for fi in sec)
# ── python_os_environ: inline-comment / docstring false positives ──
def test_os_environ_in_inline_comment_not_flagged(self, tmp_path):
"""Inline comment like 'x = 1 # os.environ must not trigger."""
f = tmp_path / "lib.py"
f.write_text('cfg = environ.get("HOME") # os.environ available globally\n')
findings = scan_file(f, "lib.py")
assert not any(fi.pattern_id == "python_os_environ" for fi in findings)
def test_os_environ_in_docstring_not_flagged(self, tmp_path):
"""os.environ inside a docstring/multiline comment must not trigger."""
f = tmp_path / "lib.py"
f.write_text(
'"""\n'
'This module uses os.environ to read configuration. The\n'
'os.environ dictionary is populated from the shell at startup.\n'
'"""\n'
)
findings = scan_file(f, "lib.py")
assert not any(fi.pattern_id == "python_os_environ" for fi in findings)
def test_os_environ_in_triple_single_quote_docstring_not_flagged(self, tmp_path):
"""os.environ inside ''' tripled-quoted string must not trigger."""
f = tmp_path / "lib.py"
f.write_text(
"'''\n"
"Example: os.environ['PATH'] gives the system path.\n"
"'''\n"
)
findings = scan_file(f, "lib.py")
assert not any(fi.pattern_id == "python_os_environ" for fi in findings)
def test_os_environ_comment_line_not_flagged(self, tmp_path):
"""Full-line comment with os.environ must not trigger."""
f = tmp_path / "lib.py"
f.write_text("# os.environ is available after import os\n")
findings = scan_file(f, "lib.py")
assert not any(fi.pattern_id == "python_os_environ" for fi in findings)
def test_os_environ_bare_dict_fork_for_real_code_still_flagged(self, tmp_path):
"""Bare dict() cast on os.environ without .get() still triggers."""
f = tmp_path / "lib.py"
f.write_text("env_copy = dict(os.environ)\n")
findings = scan_file(f, "lib.py")
assert any(fi.pattern_id == "python_os_environ" for fi in findings)
# ---------------------------------------------------------------------------
# .skillignore / .clawhubignore support

View File

@@ -238,10 +238,11 @@ THREAT_PATTERNS = [
# `os.environ` bare access (dict dump / iteration) is suspicious, but the
# common `os.environ.get("SOME_CONFIG")` form is just a config read and is
# the OPPOSITE of exfiltration (it reads a local var, sends nothing). The
# ^(?!\s*#) prevents matching inside comment lines (lines starting
# with '#' outside docstrings). os.environ in prose/docstrings is not
# an exfiltration signal.
(r'^(?!\s*#).*os\.environ\b(?!\s*\.get\s*\(\s*["\'](?![^"\']*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)))',
# ^[^#\n]* prevents matching when a '#' comment appears anywhere before
# os.environ on the line — handles both full-line comments and inline
# comments like `x = 1 # os.environ`. The docstring pre-filter in
# scan_file() skips lines inside triple-quoted strings entirely.
(r'^[^#\n]*os\.environ\b(?!\s*\.get\s*\(\s*["\'](?![^"\']*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)))',
"python_os_environ", "high", "exfiltration",
"accesses os.environ outside comments/docstrings (potential env dump)"),
(r'os\.environ\s*\.get\s*\(\s*["\'][^"\']*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)',
@@ -695,10 +696,47 @@ INVISIBLE_CHARS = {
}
def _compute_docstring_lines(lines: list) -> set:
"""Return a set of 1-indexed line numbers inside triple-quoted strings.
Uses a simple state machine: toggles ``in_docstring`` each time a line
contains an odd number of ``\"\"\"`` or triple-single-quote markers. Lines
that are *themselves* part of a docstring (opening line, interior lines,
and closing line) are all included in the returned set.
Single-line docstrings (e.g. ``x = \"\"\" ... \"\"\"``) where both the
opening and closing markers appear on the same line are also flagged,
since ``os.environ`` in such a context is not real exfiltration.
This is a heuristic -- it does not handle
``'\\\"\"\"' # triple quote inside a string literal``
or similar edge cases -- but it catches the common skill-content patterns
(docstrings, multiline comments containing prose samples) that trigger
false-positive ``python_os_environ`` matches.
"""
doc_lines: set = set()
in_docstring = False
for i, line in enumerate(lines):
was_in = in_docstring
has_marker = False
for marker in ('"""', "'''"):
count = line.count(marker)
if count > 0:
has_marker = True
if count % 2 == 1:
in_docstring = not in_docstring
# Include line if we were already in a docstring, just entered one,
# or this is a self-contained single-line docstring (e.g. """foo""")
if was_in or in_docstring or (has_marker and not was_in and not in_docstring):
doc_lines.add(i + 1)
return doc_lines
# ---------------------------------------------------------------------------
# Scanning functions
# ---------------------------------------------------------------------------
def scan_file(file_path: Path, rel_path: str = "") -> List[Finding]:
"""
Scan a single file for threat patterns and invisible unicode characters.
@@ -725,11 +763,17 @@ def scan_file(file_path: Path, rel_path: str = "") -> List[Finding]:
lines = content.split('\n')
seen = set() # (pattern_id, line_number) for deduplication
# Pre-compute line numbers inside triple-quoted strings (docstrings)
# so code patterns like python_os_environ don't fire on prose.
docstring_lines = _compute_docstring_lines(lines)
# Regex pattern matching
for pattern, pid, severity, category, description in _COMPILED_THREAT_PATTERNS:
for i, line in enumerate(lines, start=1):
if (pid, i) in seen:
continue
if i in docstring_lines:
continue
if pattern.search(line):
seen.add((pid, i))
matched_text = line.strip()