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: 1 addition & 1 deletion docker/clawscan-runtime/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ RUN git init /tmp/skillspector \
RUN python -m pip install --no-cache-dir --upgrade pip uv \
&& python -m pip install --no-cache-dir /tmp/skillspector pytest \
"cisco-ai-skill-scanner==$CISCO_AI_SKILL_SCANNER_VERSION" \
&& python -m pytest -q /tmp/clawscan-adapter-tests/test_skillspector_responses.py /tmp/clawscan-adapter-tests/test_skillspector_applicability.py /tmp/clawscan-adapter-tests/test_skillspector_accounting.py /tmp/clawscan-adapter-tests/test_skillspector_frontmatter_json.py /tmp/clawscan-adapter-tests/test_skillspector_osv_evidence.py /tmp/clawscan-adapter-tests/test_skillspector_pdf.py /tmp/clawscan-adapter-tests/test_skillspector_markdown_containers.py /tmp/clawscan-adapter-tests/test_skillspector_semantic_packets.py /tmp/clawscan-adapter-tests/test_skillspector_meta_packets.py /tmp/clawscan-adapter-tests/test_skillspector_security_view_cache.py /tmp/clawscan-adapter-tests/test_skillspector_ledger_capacity.py /tmp/clawscan-adapter-tests/test_skillspector_table_markers.py \
&& python -m pytest -q /tmp/clawscan-adapter-tests/test_skillspector_responses.py /tmp/clawscan-adapter-tests/test_skillspector_applicability.py /tmp/clawscan-adapter-tests/test_skillspector_accounting.py /tmp/clawscan-adapter-tests/test_skillspector_frontmatter_json.py /tmp/clawscan-adapter-tests/test_skillspector_osv_evidence.py /tmp/clawscan-adapter-tests/test_skillspector_pdf.py /tmp/clawscan-adapter-tests/test_skillspector_markdown_containers.py /tmp/clawscan-adapter-tests/test_skillspector_semantic_packets.py /tmp/clawscan-adapter-tests/test_skillspector_meta_packets.py /tmp/clawscan-adapter-tests/test_skillspector_security_view_cache.py /tmp/clawscan-adapter-tests/test_skillspector_ledger_capacity.py /tmp/clawscan-adapter-tests/test_skillspector_table_markers.py /tmp/clawscan-adapter-tests/test_skillspector_fence_boundaries.py \
&& uv tool install /tmp/aig/skill-scan --with pytest \
&& /root/.local/share/uv/tools/aig-skill-scan/bin/python -m pytest -q /tmp/clawscan-adapter-tests/test_aig_responses.py \
&& uv tool install "snyk-agent-scan==$SNYK_AGENT_SCAN_VERSION" \
Expand Down
27 changes: 21 additions & 6 deletions docker/clawscan-runtime/patches/skillspector-responses.patch
Original file line number Diff line number Diff line change
Expand Up @@ -1911,7 +1911,7 @@ index 74c304a..b8bc498 100644
+ assert len(requests) == 2
+ assert len(osv._cache) == 1
diff --git a/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py b/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
index c323c84..5db9eca 100644
index c323c84..49e4a5f 100644
--- a/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
+++ b/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
@@ -41,6 +41,7 @@ from skillspector.artifacts import (
Expand All @@ -1922,37 +1922,52 @@ index c323c84..5db9eca 100644
from skillspector.logging_config import get_logger
from skillspector.models import AnalyzerFinding, Finding, Location, Severity
from skillspector.python_ast import ParsedPythonFile, parse_python_source
@@ -4847,6 +4848,7 @@ def _markdown_shell_text(
@@ -4847,6 +4848,8 @@ def _markdown_shell_text(
table_columns: int | None = None
table_minimum_indent = 0
table_delimiter_line = -1
+ table_ranges: list[tuple[int, int]] = []
+ fence_ranges: list[tuple[int, int]] = []
offset = 0
lines = content.splitlines(keepends=True)
for line_index, line in enumerate(lines):
@@ -4997,6 +4999,7 @@ def _markdown_shell_text(
@@ -4921,6 +4924,13 @@ def _markdown_shell_text(
if (html_end and html_end in leading.lower()) or (not html_end and not leading):
html_end = None
elif fence is not None:
+ # A canonical container parse may end a list fence at dedentation.
+ # Keep this projection's stricter literal boundary when refining
+ # inline delimiters, just as for the table boundaries below.
+ if fence_ranges and fence_ranges[-1][1] == offset:
+ fence_ranges[-1] = (fence_ranges[-1][0], offset + len(line))
+ else:
+ fence_ranges.append((offset, offset + len(line)))
fence_body = stripped.lstrip(" \t")
closing_fence = MARKDOWN_FENCE_CLOSE.fullmatch(fence_body)
if (
@@ -4997,6 +5007,7 @@ def _markdown_shell_text(
table_minimum_indent = minimum_indent
table_delimiter_line = line_index + 1
if table_columns is not None:
+ table_ranges.append((offset, offset + len(line)))
mask_inline_delimiters()
if line_index != table_delimiter_line:
cells = _markdown_table_cells(stripped, prefix, check_runtime)
@@ -5069,6 +5072,13 @@ def _markdown_shell_text(
@@ -5069,6 +5080,13 @@ def _markdown_shell_text(
paragraph_list_indent = 0
offset += len(line)
mask_inline_delimiters()
+ if complete_context and re.search(r"(?m)^[ \t]*(?:>|[-+*][ \t]|[0-9]{1,9}[.)][ \t])", content):
+ from skillspector.markdown_tokens import MarkdownCodeSpanClosers
+
+ for delimiter in MarkdownCodeSpanClosers(
+ content, check_runtime, excluded_ranges=tuple(table_ranges)
+ content, check_runtime, excluded_ranges=tuple(table_ranges + fence_ranges)
+ ).delimiter_offsets():
+ output[delimiter] = " "
projected = "".join(output)
if prose_apostrophes:
# A prose-looking line can still be inside an earlier shell word, such
@@ -5152,6 +5162,7 @@ def _has_bounded_parse_exhaustion(
@@ -5152,6 +5170,7 @@ def _has_bounded_parse_exhaustion(
if file_type == "markdown":
if complete_context:
json_strings = validated_json_string_spans(content, check_runtime)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Conservative shell fences keep their delimiters and bounded exclusions."""

import pytest

from skillspector.markdown_tokens import MarkdownCodeSpanClosers
from skillspector.nodes.analyzers import static_patterns_tool_misuse as tool_misuse


@pytest.mark.parametrize("opening,indent", [(" ```sh", " "), ("- - ```sh", " ")])
def test_deeply_indented_marker_does_not_close_fence(opening, indent):
command = "`$(resolve_tool).example` -rf /"
content = opening + "\n" + indent + "```\n" + command + "\n```\n"
projected = tool_misuse._markdown_shell_text(content, lambda: None)
assert command in projected


def test_fence_ownership_exclusions_are_bounded_by_blocks(monkeypatch):
content = "- ```sh\n" + "\n" * 10_000 + " ```\n\n"
content += "Use `$(hostname).example` for the host.\n\n" * 100
exclusions = []
original = MarkdownCodeSpanClosers.__init__

def observe(self, *args, **kwargs):
exclusions.append(len(kwargs.get("excluded_ranges", ())))
original(self, *args, **kwargs)

monkeypatch.setattr(MarkdownCodeSpanClosers, "__init__", observe)
projected = tool_misuse._markdown_shell_text(content, lambda: None)
assert exclusions and max(exclusions) <= 1
assert "$(hostname).example" in projected
Loading