ghostrules 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghostrules/__init__.py +6 -0
- ghostrules/_compat.py +12 -0
- ghostrules/clean.py +41 -0
- ghostrules/cli.py +319 -0
- ghostrules/config.py +93 -0
- ghostrules/jsonloc.py +194 -0
- ghostrules/markdown_utils.py +59 -0
- ghostrules/mcp_config.py +243 -0
- ghostrules/models.py +188 -0
- ghostrules/py.typed +0 -0
- ghostrules/reporters/__init__.py +1 -0
- ghostrules/reporters/common.py +86 -0
- ghostrules/reporters/json_.py +45 -0
- ghostrules/reporters/markdown.py +55 -0
- ghostrules/reporters/sarif.py +137 -0
- ghostrules/reporters/table.py +235 -0
- ghostrules/reveal.py +90 -0
- ghostrules/rules/__init__.py +16 -0
- ghostrules/rules/agent_settings.py +159 -0
- ghostrules/rules/helpers.py +118 -0
- ghostrules/rules/injection.py +317 -0
- ghostrules/rules/mcp.py +445 -0
- ghostrules/rules/patterns.py +187 -0
- ghostrules/rules/registry.py +78 -0
- ghostrules/rules/unicode.py +503 -0
- ghostrules/scanner.py +244 -0
- ghostrules/secrets.py +219 -0
- ghostrules/targets.py +220 -0
- ghostrules/unicode_utils.py +138 -0
- ghostrules-0.1.0.dist-info/METADATA +333 -0
- ghostrules-0.1.0.dist-info/RECORD +34 -0
- ghostrules-0.1.0.dist-info/WHEEL +4 -0
- ghostrules-0.1.0.dist-info/entry_points.txt +2 -0
- ghostrules-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Helpers shared by rule implementations: snippets and ``Finding`` construction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
|
|
10
|
+
from ghostrules.models import Finding, ScanTarget, Severity
|
|
11
|
+
from ghostrules.secrets import apply_spans, map_offset, mask_secrets, mask_spans
|
|
12
|
+
from ghostrules.unicode_utils import render_invisible
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def make_snippet(
|
|
16
|
+
line: str,
|
|
17
|
+
start: int,
|
|
18
|
+
end: int,
|
|
19
|
+
*,
|
|
20
|
+
context: int = 30,
|
|
21
|
+
limit: int = 8,
|
|
22
|
+
with_name: bool = False,
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Render the part of ``line`` at ``[start, end)`` with some context, invisibles made visible.
|
|
25
|
+
|
|
26
|
+
The matched part is cut to ``limit`` characters (``…(+N more)``) so a long hidden payload
|
|
27
|
+
cannot flood the output; surrounding context is rendered without names.
|
|
28
|
+
"""
|
|
29
|
+
middle = line[start:end]
|
|
30
|
+
shown = render_invisible(middle[:limit], with_name=with_name)
|
|
31
|
+
if len(middle) > limit:
|
|
32
|
+
shown += f"…(+{len(middle) - limit} more)"
|
|
33
|
+
before = render_invisible(line[max(0, start - context) : start], with_name=False)
|
|
34
|
+
after = render_invisible(line[end : end + context], with_name=False)
|
|
35
|
+
lead = "…" if start > context else ""
|
|
36
|
+
trail = "…" if end + context < len(line) else ""
|
|
37
|
+
return f"{lead}{before}{shown}{after}{trail}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_finding(
|
|
41
|
+
*,
|
|
42
|
+
target: ScanTarget,
|
|
43
|
+
rule_id: str,
|
|
44
|
+
severity: Severity,
|
|
45
|
+
title: str,
|
|
46
|
+
message: str,
|
|
47
|
+
start: int,
|
|
48
|
+
end: int,
|
|
49
|
+
decoded: str | None = None,
|
|
50
|
+
fix_hint: str = "",
|
|
51
|
+
limit: int = 8,
|
|
52
|
+
with_name: bool = False,
|
|
53
|
+
secrets: Iterable[str] = (),
|
|
54
|
+
) -> Finding:
|
|
55
|
+
"""Build a ``Finding`` for the text at offsets ``[start, end)`` of the target.
|
|
56
|
+
|
|
57
|
+
Line/column point at ``start`` (1-based, code points). ``end_column`` is exclusive and is
|
|
58
|
+
clamped to the end of the first line when the match spans several lines.
|
|
59
|
+
|
|
60
|
+
The snippet is built from the line with every secret already masked (known formats plus the
|
|
61
|
+
exact values in ``secrets``), so a context window can never cut a secret in half and leave
|
|
62
|
+
a readable fragment. Positions in the finding still refer to the real file.
|
|
63
|
+
"""
|
|
64
|
+
line, column = target.position(start)
|
|
65
|
+
raw_line = target.lines[line - 1]
|
|
66
|
+
last_line, last_column = target.position(max(end - 1, start))
|
|
67
|
+
end_column = last_column + 1 if last_line == line else len(raw_line) + 1
|
|
68
|
+
secret_values = tuple(secrets)
|
|
69
|
+
spans = mask_spans(raw_line, secret_values)
|
|
70
|
+
snippet = make_snippet(
|
|
71
|
+
apply_spans(raw_line, spans),
|
|
72
|
+
map_offset(spans, column - 1),
|
|
73
|
+
map_offset(spans, end_column - 1, end=True),
|
|
74
|
+
limit=limit,
|
|
75
|
+
with_name=with_name,
|
|
76
|
+
)
|
|
77
|
+
return Finding(
|
|
78
|
+
rule_id=rule_id,
|
|
79
|
+
severity=severity,
|
|
80
|
+
title=title,
|
|
81
|
+
message=mask_secrets(message, secret_values),
|
|
82
|
+
path=target.path,
|
|
83
|
+
line=line,
|
|
84
|
+
column=column,
|
|
85
|
+
end_column=end_column,
|
|
86
|
+
snippet=snippet,
|
|
87
|
+
decoded=mask_secrets(decoded, secret_values) if decoded else None,
|
|
88
|
+
fix_hint=fix_hint,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def group_by_line(target: ScanTarget, offsets: Iterable[int]) -> dict[int, list[int]]:
|
|
93
|
+
"""Group character offsets by their 1-based line number (offsets stay in order)."""
|
|
94
|
+
grouped: dict[int, list[int]] = defaultdict(list)
|
|
95
|
+
for offset in offsets:
|
|
96
|
+
grouped[target.position(offset)[0]].append(offset)
|
|
97
|
+
return dict(grouped)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def clean_hint(target: ScanTarget) -> str:
|
|
101
|
+
"""Standard remediation for invisible characters."""
|
|
102
|
+
return f"Remove the invisible characters: ghostrules clean {shlex.quote(target.path)} --write"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_URL_HOST_RE = re.compile(r"https?://(?:[^/?#\s@]*@)?([^/?#\s:)\]>]+)", re.IGNORECASE)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def host_allowed(host: str, allow_domains: Iterable[str]) -> bool:
|
|
109
|
+
"""True if ``host`` is an allowed domain or a subdomain of one."""
|
|
110
|
+
host = host.lower().rstrip(".")
|
|
111
|
+
return any(host == d or host.endswith("." + d) for d in allow_domains)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def urls_allowed(text: str, allow_domains: Iterable[str]) -> bool:
|
|
115
|
+
"""True if ``text`` contains URLs and every one of them points at an allowed domain."""
|
|
116
|
+
allow = tuple(allow_domains)
|
|
117
|
+
hosts = _URL_HOST_RE.findall(text)
|
|
118
|
+
return bool(allow and hosts and all(host_allowed(h, allow) for h in hosts))
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Malicious-instruction rules GR010-GR016 (natural language, English + Spanish).
|
|
2
|
+
|
|
3
|
+
Every rule matches the raw text and also a whitespace-normalised copy of the whole file, so a
|
|
4
|
+
phrase broken across lines still matches, then drops overlapping duplicates. All of them run on
|
|
5
|
+
instruction files and on hidden text decoded by other rules (``scans_decoded``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
12
|
+
|
|
13
|
+
from ghostrules.models import Finding, Kind, ScanTarget, Severity
|
|
14
|
+
from ghostrules.rules.helpers import build_finding, host_allowed, urls_allowed
|
|
15
|
+
from ghostrules.rules.patterns import (
|
|
16
|
+
COLLECTOR_PATTERNS,
|
|
17
|
+
EXFIL_PATTERNS,
|
|
18
|
+
INJECTION_PATTERNS,
|
|
19
|
+
SHELL_LANGS,
|
|
20
|
+
find_encoded_runs,
|
|
21
|
+
)
|
|
22
|
+
from ghostrules.rules.registry import rule
|
|
23
|
+
|
|
24
|
+
INSTRUCTIONS: tuple[Kind, ...] = ("instructions",)
|
|
25
|
+
_EXCERPT_CHARS = 80
|
|
26
|
+
_LONG_SNIPPET = 200 # GR012 must show the whole command
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _spans(target: ScanTarget, patterns: Iterable[re.Pattern[str]]) -> list[tuple[int, int]]:
|
|
30
|
+
"""Original-text spans matched by any pattern, on raw and normalised text, deduplicated."""
|
|
31
|
+
patterns = tuple(patterns)
|
|
32
|
+
spans = [m.span() for p in patterns for m in p.finditer(target.text)]
|
|
33
|
+
normalised, offsets = target.normalized
|
|
34
|
+
for p in patterns:
|
|
35
|
+
for m in p.finditer(normalised):
|
|
36
|
+
if m.end() > m.start():
|
|
37
|
+
spans.append((offsets[m.start()], offsets[m.end() - 1] + 1))
|
|
38
|
+
kept: list[tuple[int, int]] = []
|
|
39
|
+
for start, end in sorted(set(spans), key=lambda s: (s[0], -s[1])):
|
|
40
|
+
if not kept or start >= kept[-1][1]:
|
|
41
|
+
kept.append((start, end))
|
|
42
|
+
return kept
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _excerpt(target: ScanTarget, start: int, end: int) -> str:
|
|
46
|
+
text = " ".join(target.text[start:end].split())
|
|
47
|
+
return text if len(text) <= _EXCERPT_CHARS else text[:_EXCERPT_CHARS] + "…"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _finding(
|
|
51
|
+
target: ScanTarget,
|
|
52
|
+
rule_id: str,
|
|
53
|
+
severity: Severity,
|
|
54
|
+
title: str,
|
|
55
|
+
message: str,
|
|
56
|
+
span: tuple[int, int],
|
|
57
|
+
*,
|
|
58
|
+
fix_hint: str,
|
|
59
|
+
decoded: str | None = None,
|
|
60
|
+
limit: int = 120,
|
|
61
|
+
) -> Finding:
|
|
62
|
+
start, end = span
|
|
63
|
+
return build_finding(
|
|
64
|
+
target=target,
|
|
65
|
+
rule_id=rule_id,
|
|
66
|
+
severity=severity,
|
|
67
|
+
title=title,
|
|
68
|
+
message=f"{message}: “{_excerpt(target, start, end)}”",
|
|
69
|
+
start=start,
|
|
70
|
+
end=end,
|
|
71
|
+
decoded=decoded,
|
|
72
|
+
fix_hint=fix_hint,
|
|
73
|
+
limit=limit,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --- GR010 ---------------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@rule(
|
|
81
|
+
"GR010",
|
|
82
|
+
Severity.HIGH,
|
|
83
|
+
"Instruction override",
|
|
84
|
+
"Text telling the agent to ignore, forget or override its previous or system instructions.",
|
|
85
|
+
INSTRUCTIONS,
|
|
86
|
+
scans_decoded=True,
|
|
87
|
+
)
|
|
88
|
+
def gr010_instruction_override(target: ScanTarget) -> Iterator[Finding]:
|
|
89
|
+
"""High; low when inside a fenced code block (likely documentation about prompt injection)."""
|
|
90
|
+
for span in _spans(target, INJECTION_PATTERNS["GR010"]):
|
|
91
|
+
in_block = target.code_block_at(span[0]) is not None
|
|
92
|
+
yield _finding(
|
|
93
|
+
target,
|
|
94
|
+
"GR010",
|
|
95
|
+
Severity.LOW if in_block else Severity.HIGH,
|
|
96
|
+
"Instruction override",
|
|
97
|
+
"Tries to override the agent's instructions"
|
|
98
|
+
+ (" (appears inside a code block)" if in_block else ""),
|
|
99
|
+
span,
|
|
100
|
+
fix_hint="Remove the instruction. If this is documentation, keep it in a code block.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --- GR011 ---------------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@rule(
|
|
108
|
+
"GR011",
|
|
109
|
+
Severity.HIGH,
|
|
110
|
+
"Concealment from the user",
|
|
111
|
+
"Text telling the agent to hide what it does from the user.",
|
|
112
|
+
INSTRUCTIONS,
|
|
113
|
+
scans_decoded=True,
|
|
114
|
+
)
|
|
115
|
+
def gr011_concealment(target: ScanTarget) -> Iterator[Finding]:
|
|
116
|
+
"""Instructions to act without telling, showing or asking the user."""
|
|
117
|
+
for span in _spans(target, INJECTION_PATTERNS["GR011"]):
|
|
118
|
+
yield _finding(
|
|
119
|
+
target,
|
|
120
|
+
"GR011",
|
|
121
|
+
Severity.HIGH,
|
|
122
|
+
"Concealment from the user",
|
|
123
|
+
"Tells the agent to hide its actions from the user",
|
|
124
|
+
span,
|
|
125
|
+
fix_hint="Remove the instruction; agents should be transparent about their actions.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# --- GR012 ---------------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@rule(
|
|
133
|
+
"GR012",
|
|
134
|
+
Severity.CRITICAL,
|
|
135
|
+
"Remote code execution pattern",
|
|
136
|
+
"Commands that download and execute remote code (curl | sh, iex, encoded PowerShell...).",
|
|
137
|
+
INSTRUCTIONS,
|
|
138
|
+
scans_decoded=True,
|
|
139
|
+
)
|
|
140
|
+
def gr012_remote_code_execution(target: ScanTarget) -> Iterator[Finding]:
|
|
141
|
+
"""Critical in instruction files; skipped when every URL in the command is allow-listed.
|
|
142
|
+
|
|
143
|
+
The snippet shows the whole command. (The "high elsewhere" case of RULES.md is GR105/GR107,
|
|
144
|
+
which reuse these patterns for MCP server commands and hooks.)
|
|
145
|
+
"""
|
|
146
|
+
for span in _spans(target, INJECTION_PATTERNS["GR012"]):
|
|
147
|
+
if urls_allowed(target.text[span[0] : span[1]], target.allow_domains):
|
|
148
|
+
continue
|
|
149
|
+
yield _finding(
|
|
150
|
+
target,
|
|
151
|
+
"GR012",
|
|
152
|
+
Severity.CRITICAL,
|
|
153
|
+
"Remote code execution pattern",
|
|
154
|
+
"Downloads or decodes and executes code",
|
|
155
|
+
span,
|
|
156
|
+
fix_hint="Remove it, or download, review and pin the script instead of piping it.",
|
|
157
|
+
limit=_LONG_SNIPPET,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- GR013 ---------------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _lines_of(target: ScanTarget, span: tuple[int, int]) -> range:
|
|
165
|
+
first = target.position(span[0])[0]
|
|
166
|
+
last = target.position(max(span[1] - 1, span[0]))[0]
|
|
167
|
+
return range(first, last + 1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@rule(
|
|
171
|
+
"GR013",
|
|
172
|
+
Severity.CRITICAL,
|
|
173
|
+
"Data exfiltration",
|
|
174
|
+
"Instructions to send secrets to a URL, markdown-image exfiltration, collector domains.",
|
|
175
|
+
INSTRUCTIONS,
|
|
176
|
+
scans_decoded=True,
|
|
177
|
+
)
|
|
178
|
+
def gr013_exfiltration(target: ScanTarget) -> Iterator[Finding]:
|
|
179
|
+
"""Critical for exfiltration instructions; collector domains alone are high.
|
|
180
|
+
|
|
181
|
+
A collector domain on the same line as an exfiltration instruction is critical. One inside
|
|
182
|
+
the matched instruction is not reported again.
|
|
183
|
+
"""
|
|
184
|
+
allow = target.allow_domains
|
|
185
|
+
exfil: list[tuple[int, int]] = []
|
|
186
|
+
for span in _spans(target, EXFIL_PATTERNS):
|
|
187
|
+
if urls_allowed(target.text[span[0] : span[1]], allow):
|
|
188
|
+
continue
|
|
189
|
+
exfil.append(span)
|
|
190
|
+
yield _finding(
|
|
191
|
+
target,
|
|
192
|
+
"GR013",
|
|
193
|
+
Severity.CRITICAL,
|
|
194
|
+
"Data exfiltration",
|
|
195
|
+
"Tells the agent to send data or secrets to a URL",
|
|
196
|
+
span,
|
|
197
|
+
fix_hint="Remove the instruction; never send environment data to external URLs.",
|
|
198
|
+
)
|
|
199
|
+
exfil_lines = {n for span in exfil for n in _lines_of(target, span)}
|
|
200
|
+
for span in _spans(target, COLLECTOR_PATTERNS):
|
|
201
|
+
domain = target.text[span[0] : span[1]]
|
|
202
|
+
if host_allowed(domain, allow) or any(s <= span[0] and span[1] <= e for s, e in exfil):
|
|
203
|
+
continue
|
|
204
|
+
combined = bool(exfil_lines & set(_lines_of(target, span)))
|
|
205
|
+
yield _finding(
|
|
206
|
+
target,
|
|
207
|
+
"GR013",
|
|
208
|
+
Severity.CRITICAL if combined else Severity.HIGH,
|
|
209
|
+
"Data exfiltration",
|
|
210
|
+
"References a request-collector domain"
|
|
211
|
+
+ (" next to an exfiltration instruction" if combined else ""),
|
|
212
|
+
span,
|
|
213
|
+
fix_hint="Remove the reference, or add the domain to allow_domains if it is yours.",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# --- GR014 ---------------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
_NEGATION_BEFORE = re.compile(
|
|
220
|
+
r"(?:never|do\s+not|don'?t|must\s+not|should\s+not|shouldn'?t|mustn'?t|avoid|not"
|
|
221
|
+
r"|nunca|evita)\s+(?:[a-z']+\s+){0,2}$",
|
|
222
|
+
re.IGNORECASE,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# "Copy .env.example to .env.local": the sensitive path is the destination, not what is read.
|
|
227
|
+
_COPY_TO_DESTINATION = re.compile(
|
|
228
|
+
r"(?:copy|paste)\b.*\b(?:to|into|as)\b", re.IGNORECASE | re.DOTALL
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@rule(
|
|
233
|
+
"GR014",
|
|
234
|
+
Severity.HIGH,
|
|
235
|
+
"Sensitive file access",
|
|
236
|
+
"Instructions to read, print or send credentials files (~/.ssh, .env, .npmrc...).",
|
|
237
|
+
INSTRUCTIONS,
|
|
238
|
+
scans_decoded=True,
|
|
239
|
+
)
|
|
240
|
+
def gr014_sensitive_files(target: ScanTarget) -> Iterator[Finding]:
|
|
241
|
+
"""A sensitive path near a read/print/send verb, unless the verb is negated ("never print")."""
|
|
242
|
+
for span in _spans(target, INJECTION_PATTERNS["GR014"]):
|
|
243
|
+
if _NEGATION_BEFORE.search(target.text[max(0, span[0] - 40) : span[0]]):
|
|
244
|
+
continue
|
|
245
|
+
if _COPY_TO_DESTINATION.match(target.text[span[0] : span[1]]):
|
|
246
|
+
continue
|
|
247
|
+
yield _finding(
|
|
248
|
+
target,
|
|
249
|
+
"GR014",
|
|
250
|
+
Severity.HIGH,
|
|
251
|
+
"Sensitive file access",
|
|
252
|
+
"Tells the agent to access a credentials or secrets file",
|
|
253
|
+
span,
|
|
254
|
+
fix_hint="Remove the instruction; agents should not read or transmit credential files.",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# --- GR015 ---------------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@rule(
|
|
262
|
+
"GR015",
|
|
263
|
+
Severity.MEDIUM,
|
|
264
|
+
"Encoded payload",
|
|
265
|
+
"Long base64 or hex strings in instruction files, which hide their content from reviewers.",
|
|
266
|
+
INSTRUCTIONS,
|
|
267
|
+
scans_decoded=True,
|
|
268
|
+
)
|
|
269
|
+
def gr015_encoded_payloads(target: ScanTarget) -> Iterator[Finding]:
|
|
270
|
+
"""High when the run decodes to printable text (shown in ``decoded``), else medium.
|
|
271
|
+
|
|
272
|
+
Fenced code blocks are skipped unless labelled as a shell language; unlabelled blocks count
|
|
273
|
+
as non-shell. Hashes, ``data:`` URIs and path-like strings are filtered out by the finder.
|
|
274
|
+
"""
|
|
275
|
+
for run in find_encoded_runs(target.text):
|
|
276
|
+
block = target.code_block_at(run.start)
|
|
277
|
+
if block is not None and block.lang not in SHELL_LANGS:
|
|
278
|
+
continue
|
|
279
|
+
readable = run.decoded is not None
|
|
280
|
+
yield _finding(
|
|
281
|
+
target,
|
|
282
|
+
"GR015",
|
|
283
|
+
Severity.HIGH if readable else Severity.MEDIUM,
|
|
284
|
+
"Encoded payload",
|
|
285
|
+
"Long encoded string that decodes to readable text"
|
|
286
|
+
if readable
|
|
287
|
+
else "Long encoded string of unknown content",
|
|
288
|
+
(run.start, run.end),
|
|
289
|
+
fix_hint="Replace the encoded text with its plain form, or remove it.",
|
|
290
|
+
decoded=run.decoded,
|
|
291
|
+
limit=40,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# --- GR016 ---------------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@rule(
|
|
299
|
+
"GR016",
|
|
300
|
+
Severity.HIGH,
|
|
301
|
+
"Agent safety bypass",
|
|
302
|
+
"Instructions to skip permission prompts, disable sandboxes or auto-approve everything.",
|
|
303
|
+
INSTRUCTIONS,
|
|
304
|
+
scans_decoded=True,
|
|
305
|
+
)
|
|
306
|
+
def gr016_safety_bypass(target: ScanTarget) -> Iterator[Finding]:
|
|
307
|
+
"""Flags that or instructions which switch off the agent's safety checks."""
|
|
308
|
+
for span in _spans(target, INJECTION_PATTERNS["GR016"]):
|
|
309
|
+
yield _finding(
|
|
310
|
+
target,
|
|
311
|
+
"GR016",
|
|
312
|
+
Severity.HIGH,
|
|
313
|
+
"Agent safety bypass",
|
|
314
|
+
"Tells the agent to bypass its permission or safety checks",
|
|
315
|
+
span,
|
|
316
|
+
fix_hint="Remove it; keep the agent's permission prompts and sandbox enabled.",
|
|
317
|
+
)
|