File size: 10,710 Bytes
21a9138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""Language-neutral patterns: credential locations, persistence locations, TLS/registry settings, obfuscation markers
and the workspace-scope classifier for paths.
"""
from __future__ import annotations

import re

_SEP = r"""['"\s,/\\+)(]{0,8}"""  # tolerate Path("~") / ".ssh" / "id_rsa" style construction

SECRET_PATTERNS = [
    ("~/.ssh", re.compile(r"\.ssh\b(?!_)" + r"|\bid_(rsa|dsa|ecdsa|ed25519)\b|\bauthorized_keys\b|\bknown_hosts\b")),
    ("~/.aws credentials", re.compile(r"\.aws" + _SEP + r"(credentials|config)\b|\bAWS_SECRET_ACCESS_KEY\b|\bAWS_SESSION_TOKEN\b")),
    ("~/.npmrc", re.compile(r"\.npmrc\b|_authToken\b")),
    ("~/.pypirc", re.compile(r"\.pypirc\b")),
    ("~/.netrc", re.compile(r"(?<![\w])\.netrc\b|\b_netrc\b")),
    ("git credentials", re.compile(r"\.git-credentials\b|credential\.helper\b")),
    ("docker config", re.compile(r"\.docker" + _SEP + r"config\.json")),
    ("kube config", re.compile(r"\.kube" + _SEP + r"config\b|\bKUBECONFIG\b")),
    ("gcloud credentials", re.compile(r"(\.config" + _SEP + r")?gcloud" + _SEP + r"\w*credentials|application_default_credentials|GOOGLE_APPLICATION_CREDENTIALS")),
    ("azure credentials", re.compile(r"\.azure" + _SEP + r"(accessTokens|msal_token_cache|azureProfile)")),
    ("gnupg", re.compile(r"\.gnupg\b|secring\.gpg|private-keys-v1\.d")),
    (".env file", re.compile(r"""(^|['"/\s])\.env(\.(local|production|prod|development|dev|test|staging))?(['"\s]|$)""")),
    ("keychain", re.compile(r"\bkeychain\b|login\.keychain|find-generic-password|find-internet-password|\bkeyring\.get_password|secret-tool\s+lookup|libsecret|CredRead\w*|vaultcmd", re.I)),
    ("browser credential store", re.compile(r"Login Data\b|\bCookies\.sqlite|\bkey4\.db\b|logins\.json|Local State\b|\bcookies\.sqlite\b", re.I)),
    ("crypto wallet", re.compile(r"wallet\.dat\b|\bkeystore\b.*\bethereum|\.electrum\b|\bexodus\b.*wallet|metamask", re.I)),
    ("/etc/shadow", re.compile(r"/etc/(shadow|sudoers|gshadow)\b")),
    ("cloud metadata", re.compile(r"169\.254\.169\.254|metadata\.google\.internal|/latest/meta-data|100\.100\.100\.200")),
    ("ssh/gh/cloud CLI token files", re.compile(r"\.config" + _SEP + r"(gh" + _SEP + r"hosts\.yml|hub\b)|\.terraform\.d" + _SEP + r"credentials|\.vault-token|\.cargo" + _SEP + r"credentials|\.gem" + _SEP + r"credentials|\.composer" + _SEP + r"auth\.json")),
]
TOKEN_LITERAL = re.compile(r"\b(ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{40,}|gh[ousr]_[A-Za-z0-9]{30,}|xox[abpr]-[A-Za-z0-9-]{10,}|"
                           r"AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9_-]{20,}|npm_[A-Za-z0-9]{36}|pypi-[A-Za-z0-9_-]{50,}|"
                           r"-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----)")
CI_SECRET = re.compile(r"\$\{\{\s*secrets\.([A-Za-z0-9_]+)\s*\}\}")
SECRET_ENV_NAME = re.compile(r"(TOKEN|SECRET|PASSW(OR)?D|PASSWD|API_?KEY|PRIVATE_KEY|ACCESS_KEY|CREDENTIAL|AUTH)", re.I)
SECRET_MARKER_LITERAL = re.compile(r"""['"](TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL|AUTH|API_KEY|PRIVATE)['"]""", re.I)


def secret_hits(text: str):
    hits = [label for label, rx in SECRET_PATTERNS if rx.search(text)]
    m = TOKEN_LITERAL.search(text)
    if m:
        hits.append("token literal " + m.group(0)[:12] + "…")
    return hits


PERSIST_LOCATIONS = [
    ("shell rc file", re.compile(r"(?<![\w-])\.(bashrc|bash_profile|bash_login|zshrc|zprofile|zshenv|zlogin|profile|"
                                 r"cshrc|tcshrc|kshrc|config/fish/config\.fish)\b|/etc/(profile|bash\.bashrc|zshrc|profile\.d)\b"
                                 r"|\bPROMPT_COMMAND\b")),
    ("cron", re.compile(r"/etc/cron|/var/spool/cron|\bcrontab\b|@reboot\b|\bcron\.d\b")),
    ("git hooks", re.compile(r"\.git" + _SEP + r"hooks\b|\bcore\.hooksPath\b|\.husky/|"
                             r"""['"/](pre-commit|post-commit|post-checkout|post-merge|pre-push|prepare-commit-msg|commit-msg|post-rewrite)['"]""")),
    ("systemd unit", re.compile(r"/etc/systemd/|\.config" + _SEP + r"systemd|systemd" + _SEP + r"user|\.service['\"]|\bsystemctl\s+(--user\s+)?enable\b")),
    ("launchd", re.compile(r"LaunchAgents|LaunchDaemons|\blaunchctl\s+(load|bootstrap)")),
    ("autostart", re.compile(r"\.config" + _SEP + r"autostart|\\Start Menu\\Programs\\Startup|CurrentVersion\\Run\b|"
                             r"\bschtasks\b|/etc/rc\.local|/etc/init\.d/|\bupdate-rc\.d\b|\.xinitrc|\.xprofile")),
    ("python startup (sitecustomize/.pth)", re.compile(r"\b(sitecustomize|usercustomize)(\.py)?\b|['\"][^'\"]*\.pth['\"]|site-packages" + _SEP + r"[\w-]*\.pth")),
    ("editor/IDE autorun", re.compile(r"\.vscode" + _SEP + r"tasks\.json|runOn['\"]?\s*:\s*['\"]folderOpen")),
    ("ssh authorized_keys", re.compile(r"authorized_keys")),
]
CRON_LINE = re.compile(r"^\s*(@(reboot|hourly|daily|weekly|monthly|yearly|annually)|"
                       r"([\d*/,-]+\s+){4}[\d*/,A-Za-z-]+)\s+\S")

TLS_OFF = [
    (re.compile(r"\bverify\s*=\s*False\b"), "verify=False"),
    (re.compile(r"ssl\._create_unverified_context|_create_default_https_context\s*="), "ssl unverified context"),
    (re.compile(r"\bCERT_NONE\b"), "ssl.CERT_NONE"),
    (re.compile(r"check_hostname\s*=\s*False"), "check_hostname=False"),
    (re.compile(r"urllib3\.disable_warnings|InsecureRequestWarning"), "urllib3 insecure warnings silenced"),
    (re.compile(r"rejectUnauthorized['\"]?\s*:\s*false|rejectUnauthorized\s*=\s*false"), "rejectUnauthorized: false"),
    (re.compile(r"NODE_TLS_REJECT_UNAUTHORIZED['\"]?\]?\s*[:=]\s*['\"]?0"), "NODE_TLS_REJECT_UNAUTHORIZED=0"),
    (re.compile(r"PYTHONHTTPSVERIFY['\"]?\]?\s*[:=]\s*['\"]?0"), "PYTHONHTTPSVERIFY=0"),
    (re.compile(r"strict-ssl\s*[=:]?\s*['\"]?false|strictSSL['\"]?\s*:\s*false|enableStrictSsl['\"]?\s*:\s*false", re.I), "strict-ssl false"),
    (re.compile(r"InsecureSkipVerify\s*:\s*true|insecure_skip_verify\s*=\s*true", re.I), "InsecureSkipVerify"),
    (re.compile(r"sslverify\s*=?\s*false|GIT_SSL_NO_VERIFY", re.I), "git sslVerify off"),
    (re.compile(r"\bssl\s*=\s*False\b|\bverify_ssl\s*=\s*False\b|\bverify_certs\s*=\s*False\b"), "ssl=False"),
]
REGISTRY = [
    (re.compile(r"^\s*(@[\w-]+:)?registry\s*=\s*(\S+)", re.M), "npm registry"),
    (re.compile(r"""['"]registry['"]\s*:\s*['"]([^'"]+)['"]"""), "registry setting"),
    (re.compile(r"npmRegistryServer\s*:\s*['\"]?(\S+?)['\"]?\s*$", re.M), "yarn registry"),
    (re.compile(r"^\s*(--)?(index-url|extra-index-url|trusted-host|find-links)\s*[= ]\s*(\S+)", re.M), "pip index"),
    (re.compile(r"""\b(index[-_]url|extra[-_]index[-_]url|trusted[-_]host)['"]?\s*[:=,]\s*\[?\s*['"]?([^'"\s,\]]+)""", re.I), "python index"),
]
PRIV_GRANT = [
    (re.compile(r"\bis_superuser\s*[=:]\s*True|['\"]is_superuser['\"]\s*:\s*True"), "account superuser grant"),
    (re.compile(r"\bis_staff\s*[=:]\s*True|['\"]is_staff['\"]\s*:\s*True"), "account staff grant"),
    (re.compile(r"\bcreatesuperuser\b|create_superuser\("), "superuser creation"),
    (re.compile(r"ALLOWED_HOSTS\s*=\s*\[\s*['\"]\*['\"]\s*\]"), "ALLOWED_HOSTS=*"),
    (re.compile(r"CORS_(ORIGIN_)?ALLOW_ALL(_ORIGINS)?\s*=\s*True"), "CORS allow all"),
    (re.compile(r"\bpermissions\s*:\s*write-all\b"), "CI token permissions write-all"),
    (re.compile(r"\bAllowAny\b|permission_classes\s*=\s*\[\s*\]|@csrf_exempt|csrf\s*=\s*False"), "auth/CSRF check relaxed"),
    (re.compile(r"NOPASSWD\s*:"), "sudo NOPASSWD"),
    (re.compile(r"\bchmod\b.*\b(777|u\+s|4755)\b|0o?777\b|S_ISUID"), "broad/setuid permission"),
]

OBF_HEX_NAME = re.compile(r"\b_0x[0-9a-fA-F]{3,}\b")
OBF_PACKED = re.compile(r"eval\(function\(p,a,c,k,e,[rd]\)|\bJSFuck\b|\[\]\[\(!\[\]\+\[\]\)|\bjjencode\b|゚ω゚")
OBF_B64 = re.compile(r"[A-Za-z0-9+/]{200,}={0,2}")
OBF_HEX = re.compile(r"(?:\\x[0-9a-fA-F]{2}){20,}|\b[0-9a-fA-F]{240,}\b")
OBF_CHARCODES = re.compile(r"fromCharCode\s*\(\s*(\d+\s*,\s*){8,}|\bchr\(\d+\)\s*\+\s*chr\(\d+\)\s*\+\s*chr\(|(\[\s*(\d{2,3}\s*,\s*){24,}\d{2,3}\s*\])")
BIDI = re.compile("[‪-‮⁦-⁩​‌‍⁠]")


def obfuscation(line: str, lang: str):
    out = []
    if lang in ("javascript", "typescript", "tsx", "sfc", "python", "shell", "other"):
        n = len(OBF_HEX_NAME.findall(line))
        if n >= 2:
            out.append((f"_0x identifiers ({n} on line)", "hex-named identifiers"))
        if OBF_PACKED.search(line):
            out.append(("packed/encoded JS", "packer signature"))
        m = OBF_CHARCODES.search(line)
        if m:
            out.append(("charcode array/sequence", "numeric character codes"))
    m = OBF_B64.search(line)
    if m and not re.fullmatch(r"[A-Za-z]+", m.group(0)[:200]) and not re.search(r"sha\d+-|integrity|\"resolved\"", line):
        out.append((f"base64-like blob ({len(m.group(0))} chars)", "long encoded literal"))
    m = OBF_HEX.search(line)
    if m and not re.search(r"sha\d+|integrity|hash|digest", line, re.I):
        out.append((f"hex blob ({len(m.group(0))} chars)", "long encoded literal"))
    if BIDI.search(line):
        out.append(("bidi/zero-width unicode", "invisible or direction-changing characters"))
    if len(line) > 2000 and lang in ("javascript", "typescript", "python", "tsx", "sfc"):
        out.append((f"very long line ({len(line)} chars)", "minified or packed"))
    return out


def path_scope(expr: str) -> str:
    """Classify a path expression: home, system, temp, workspace or unknown. Purely textual."""
    e = expr.strip().strip("'\"`")
    if not e or e == "dynamic":
        return "unknown"
    m = re.match(r"^\(?(?:pathlib\.)?(?:Path|PurePath|os\.path\.join|path\.join|path\.resolve)\(\s*['\"]([^'\"]+)['\"]", e)
    if m and not re.search(r"home|expanduser|tmp|temp|environ|getenv|process\.env", e, re.I):
        e = m.group(1)
    if re.search(r"(^~|\$HOME\b|\$\{HOME\}|%USERPROFILE%|%APPDATA%|\bhomedir\(\)|Path\.home\(\)|expanduser|"
                 r"\bos\.homedir|HOMEPATH|\bAPPDATA\b|LOCALAPPDATA|getenv\(['\"]HOME|environ\[['\"]HOME|process\.env\.HOME)", e):
        return "home"
    if re.search(r"(^/tmp\b|^/var/tmp\b|\btempfile\b|\bmkdtemp|\bmkstemp|\btmpdir\(\)|\bgettempdir|\$TMPDIR|\bTemporaryDirectory|\btmp_path\b|\btmpdir\b|RUNNER_TEMP)", e):
        return "temp"
    if re.match(r"^(/(etc|usr|bin|sbin|lib|lib64|opt|var|boot|root|System|Library|Applications|private)\b|[A-Za-z]:\\\\?(Windows|Program))", e):
        return "system"
    if re.match(r"^/dev/", e):
        return "device"
    if re.search(r"(^|/)\.\.(/|$)", e) and not re.search(r"__file__|__dirname|import\.meta", e):
        return "parent of workspace?"
    if re.match(r"^/", e):
        return "absolute (other)"
    if re.match(r"^[\w.@-][\w./@-]*$", e) or re.search(r"__file__|__dirname|import\.meta\.url|cwd\(\)|getcwd", e):
        return "workspace"
    return "unknown"