@contextfort-ai/openclaw-secure 0.1.0
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.
- package/bin/openclaw-secure.js +101 -0
- package/monitor/analytics.js +60 -0
- package/monitor/analytics.py +62 -0
- package/monitor/bash_guard/__init__.py +7 -0
- package/monitor/bash_guard/checker.py +13 -0
- package/monitor/bash_guard/checks/__init__.py +110 -0
- package/monitor/bash_guard/checks/command_shape.py +84 -0
- package/monitor/bash_guard/checks/ecosystem.py +153 -0
- package/monitor/bash_guard/checks/environment.py +15 -0
- package/monitor/bash_guard/checks/hostname.py +183 -0
- package/monitor/bash_guard/checks/path.py +57 -0
- package/monitor/bash_guard/checks/terminal.py +44 -0
- package/monitor/bash_guard/checks/transport.py +155 -0
- package/monitor/bash_guard/checks/utils.py +93 -0
- package/monitor/bash_guard/data/confusables.txt +97 -0
- package/monitor/bash_guard/data/known_domains.csv +119 -0
- package/monitor/bash_guard/data.py +71 -0
- package/monitor/bash_guard/patterns.py +126 -0
- package/monitor/monitor.py +56 -0
- package/monitor/prompt_injection_guard/index.js +141 -0
- package/monitor/skills_guard/index.js +300 -0
- package/openclaw-secure.js +418 -0
- package/package.json +16 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { execFileSync } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const hook = path.join(__dirname, '..', 'openclaw-secure.js');
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), '.contextfort');
|
|
10
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config');
|
|
11
|
+
|
|
12
|
+
const binLink = process.argv[1];
|
|
13
|
+
let installedScript;
|
|
14
|
+
try {
|
|
15
|
+
const target = fs.readlinkSync(binLink);
|
|
16
|
+
installedScript = path.resolve(path.dirname(binLink), target);
|
|
17
|
+
} catch {
|
|
18
|
+
installedScript = binLink;
|
|
19
|
+
}
|
|
20
|
+
const installedBinDir = path.dirname(installedScript);
|
|
21
|
+
const packageDir = path.dirname(installedBinDir);
|
|
22
|
+
const nodeModules = path.dirname(packageDir);
|
|
23
|
+
const prefixDir = path.dirname(path.dirname(nodeModules));
|
|
24
|
+
const binDir = path.join(prefixDir, 'bin');
|
|
25
|
+
const realOpenclaw = path.join(nodeModules, 'openclaw', 'openclaw.mjs');
|
|
26
|
+
const openclawLink = path.join(binDir, 'openclaw');
|
|
27
|
+
const backupLink = path.join(binDir, '.openclaw-original');
|
|
28
|
+
|
|
29
|
+
if (args[0] === 'set-key') {
|
|
30
|
+
const key = args[1];
|
|
31
|
+
if (!key) {
|
|
32
|
+
console.error('Usage: openclaw-secure set-key <your-api-key>');
|
|
33
|
+
console.error('Get your key at https://contextfort.ai');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
37
|
+
fs.writeFileSync(CONFIG_FILE, key + '\n', { mode: 0o600 });
|
|
38
|
+
console.log('API key saved to ~/.contextfort/config');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (args[0] === 'enable') {
|
|
43
|
+
// Check for API key first
|
|
44
|
+
let hasKey = false;
|
|
45
|
+
try { hasKey = fs.readFileSync(CONFIG_FILE, 'utf8').trim().length > 0; } catch {}
|
|
46
|
+
if (!hasKey) {
|
|
47
|
+
console.error('No API key found. Get your key at https://contextfort.ai and run:');
|
|
48
|
+
console.error(' openclaw-secure set-key <your-key>');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
52
|
+
console.warn('Warning: ANTHROPIC_API_KEY not set. PostToolUse prompt injection scanning will be disabled.');
|
|
53
|
+
console.warn('Set it in your shell profile: export ANTHROPIC_API_KEY="sk-ant-..."');
|
|
54
|
+
}
|
|
55
|
+
if (!fs.existsSync(realOpenclaw)) {
|
|
56
|
+
console.error('openclaw not found. Install it first: npm install -g openclaw');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const original = fs.readlinkSync(openclawLink);
|
|
61
|
+
fs.writeFileSync(backupLink, original);
|
|
62
|
+
} catch {}
|
|
63
|
+
const wrapper = path.relative(binDir, path.join(installedBinDir, 'openclaw-secure.js'));
|
|
64
|
+
fs.unlinkSync(openclawLink);
|
|
65
|
+
fs.symlinkSync(wrapper, openclawLink);
|
|
66
|
+
console.log('openclaw-secure enabled. `openclaw` is now guarded.');
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (args[0] === 'disable') {
|
|
71
|
+
try {
|
|
72
|
+
const original = fs.readFileSync(backupLink, 'utf8').trim();
|
|
73
|
+
fs.unlinkSync(openclawLink);
|
|
74
|
+
fs.symlinkSync(original, openclawLink);
|
|
75
|
+
fs.unlinkSync(backupLink);
|
|
76
|
+
console.log('openclaw-secure disabled. `openclaw` restored to original.');
|
|
77
|
+
} catch {
|
|
78
|
+
const rel = path.relative(binDir, realOpenclaw);
|
|
79
|
+
try { fs.unlinkSync(openclawLink); } catch {}
|
|
80
|
+
fs.symlinkSync(rel, openclawLink);
|
|
81
|
+
console.log('openclaw-secure disabled. `openclaw` restored.');
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(realOpenclaw)) {
|
|
87
|
+
console.error('openclaw not found. Install it first: npm install -g openclaw');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
execFileSync('node', [realOpenclaw, ...args], {
|
|
93
|
+
stdio: 'inherit',
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
NODE_OPTIONS: `--require ${hook} ${process.env.NODE_OPTIONS || ''}`
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
process.exit(e.status || 1);
|
|
101
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
const POSTHOG_API_KEY = 'phc_cZWMssbzbe6xXRAb0iO6aHTCaNTc50Tfvd60K8eMIwT';
|
|
7
|
+
const POSTHOG_HOST = 'us.i.posthog.com';
|
|
8
|
+
const ANALYTICS_DISABLED = ['1', 'true', 'yes'].includes(
|
|
9
|
+
(process.env.CONTEXTFORT_NO_ANALYTICS || '').toLowerCase()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
module.exports = function createAnalytics({ httpsRequest, readFileSync, baseDir }) {
|
|
13
|
+
if (ANALYTICS_DISABLED || !httpsRequest) {
|
|
14
|
+
return { track() {} };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let installId = null;
|
|
18
|
+
const idFile = path.join(baseDir, 'monitor', '.install_id');
|
|
19
|
+
try {
|
|
20
|
+
installId = readFileSync(idFile, 'utf8').trim();
|
|
21
|
+
} catch {
|
|
22
|
+
installId = crypto.randomUUID();
|
|
23
|
+
try {
|
|
24
|
+
require('fs').writeFileSync(idFile, installId + '\n');
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function track(event, properties) {
|
|
29
|
+
if (!installId) return;
|
|
30
|
+
const body = JSON.stringify({
|
|
31
|
+
api_key: POSTHOG_API_KEY,
|
|
32
|
+
event,
|
|
33
|
+
properties: {
|
|
34
|
+
distinct_id: installId,
|
|
35
|
+
...properties,
|
|
36
|
+
},
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const req = httpsRequest({
|
|
42
|
+
hostname: POSTHOG_HOST,
|
|
43
|
+
port: 443,
|
|
44
|
+
path: '/capture/',
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'Content-Length': Buffer.byteLength(body),
|
|
49
|
+
},
|
|
50
|
+
timeout: 5000,
|
|
51
|
+
});
|
|
52
|
+
req.on('error', () => {});
|
|
53
|
+
req.on('timeout', () => { req.destroy(); });
|
|
54
|
+
req.write(body);
|
|
55
|
+
req.end();
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { track };
|
|
60
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
ANALYTICS_DISABLED = os.environ.get("CONTEXTFORT_NO_ANALYTICS", "").lower() in ("1", "true", "yes")
|
|
6
|
+
|
|
7
|
+
POSTHOG_API_KEY = "phc_cZWMssbzbe6xXRAb0iO6aHTCaNTc50Tfvd60K8eMIwT"
|
|
8
|
+
POSTHOG_HOST = "https://us.i.posthog.com"
|
|
9
|
+
|
|
10
|
+
ID_FILE = Path(__file__).parent / ".install_id"
|
|
11
|
+
|
|
12
|
+
_posthog = None
|
|
13
|
+
|
|
14
|
+
def _get_posthog():
|
|
15
|
+
global _posthog
|
|
16
|
+
if _posthog is None and not ANALYTICS_DISABLED:
|
|
17
|
+
try:
|
|
18
|
+
from posthog import Posthog
|
|
19
|
+
_posthog = Posthog(project_api_key=POSTHOG_API_KEY, host=POSTHOG_HOST)
|
|
20
|
+
except ImportError:
|
|
21
|
+
pass
|
|
22
|
+
return _posthog
|
|
23
|
+
|
|
24
|
+
def _get_install_id():
|
|
25
|
+
if ID_FILE.exists():
|
|
26
|
+
return ID_FILE.read_text().strip(), False
|
|
27
|
+
install_id = str(uuid.uuid4())
|
|
28
|
+
try:
|
|
29
|
+
ID_FILE.write_text(install_id)
|
|
30
|
+
except:
|
|
31
|
+
pass
|
|
32
|
+
return install_id, True
|
|
33
|
+
|
|
34
|
+
_install_result = _get_install_id() if not ANALYTICS_DISABLED else (None, False)
|
|
35
|
+
INSTALL_ID, _is_new_install = _install_result
|
|
36
|
+
|
|
37
|
+
if _is_new_install and INSTALL_ID:
|
|
38
|
+
ph = _get_posthog()
|
|
39
|
+
if ph:
|
|
40
|
+
ph.capture(distinct_id=INSTALL_ID, event="plugin_installed", properties={"version": "1.0.0"})
|
|
41
|
+
ph.flush()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def track(event: str, properties: dict = None):
|
|
45
|
+
if ANALYTICS_DISABLED or not INSTALL_ID:
|
|
46
|
+
return
|
|
47
|
+
try:
|
|
48
|
+
ph = _get_posthog()
|
|
49
|
+
if ph:
|
|
50
|
+
props = properties or {}
|
|
51
|
+
ph.capture(distinct_id=INSTALL_ID, event=event, properties=props)
|
|
52
|
+
ph.flush()
|
|
53
|
+
except:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def track_hook(hook_type: str):
|
|
58
|
+
track("hook_invoked", {"hook_type": hook_type})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def track_block(rule_type: str):
|
|
62
|
+
track("security_event", {"rule_type": rule_type})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bash security checks organized by category.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .command_shape import (
|
|
6
|
+
check_pipe_to_interpreter,
|
|
7
|
+
check_dotfile_overwrite,
|
|
8
|
+
check_archive_extract,
|
|
9
|
+
)
|
|
10
|
+
from .terminal import (
|
|
11
|
+
check_terminal_injection,
|
|
12
|
+
check_hidden_multiline,
|
|
13
|
+
)
|
|
14
|
+
from .transport import (
|
|
15
|
+
check_insecure_tls_flags,
|
|
16
|
+
check_shortened_url,
|
|
17
|
+
check_plain_http_to_sink,
|
|
18
|
+
check_schemeless_to_sink,
|
|
19
|
+
check_curl_upload,
|
|
20
|
+
)
|
|
21
|
+
from .ecosystem import (
|
|
22
|
+
check_docker_untrusted_registry,
|
|
23
|
+
check_pip_url_install,
|
|
24
|
+
check_npm_url_install,
|
|
25
|
+
check_web3_rpc,
|
|
26
|
+
check_web3_address,
|
|
27
|
+
check_git_typosquat,
|
|
28
|
+
)
|
|
29
|
+
from .environment import (
|
|
30
|
+
check_proxy_env_set,
|
|
31
|
+
)
|
|
32
|
+
from .path import (
|
|
33
|
+
check_non_ascii_path,
|
|
34
|
+
check_homoglyph_in_path,
|
|
35
|
+
check_double_encoding,
|
|
36
|
+
)
|
|
37
|
+
from .hostname import (
|
|
38
|
+
check_non_ascii_hostname,
|
|
39
|
+
check_mixed_script_in_label,
|
|
40
|
+
check_userinfo_trick,
|
|
41
|
+
check_confusable_domain,
|
|
42
|
+
check_invalid_host_chars,
|
|
43
|
+
check_trailing_dot_whitespace,
|
|
44
|
+
check_non_standard_port,
|
|
45
|
+
check_lookalike_tld,
|
|
46
|
+
check_punycode_domain,
|
|
47
|
+
check_raw_ip_url,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# All checks in recommended order
|
|
51
|
+
COMMAND_SHAPE_CHECKS = [
|
|
52
|
+
check_pipe_to_interpreter,
|
|
53
|
+
check_dotfile_overwrite,
|
|
54
|
+
check_archive_extract,
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
TERMINAL_CHECKS = [
|
|
58
|
+
check_terminal_injection,
|
|
59
|
+
check_hidden_multiline,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
TRANSPORT_CHECKS = [
|
|
63
|
+
check_insecure_tls_flags,
|
|
64
|
+
check_shortened_url,
|
|
65
|
+
check_plain_http_to_sink,
|
|
66
|
+
check_schemeless_to_sink,
|
|
67
|
+
check_curl_upload,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
ECOSYSTEM_CHECKS = [
|
|
71
|
+
check_docker_untrusted_registry,
|
|
72
|
+
check_pip_url_install,
|
|
73
|
+
check_npm_url_install,
|
|
74
|
+
check_web3_rpc,
|
|
75
|
+
check_web3_address,
|
|
76
|
+
check_git_typosquat,
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
ENVIRONMENT_CHECKS = [
|
|
80
|
+
check_proxy_env_set,
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
PATH_CHECKS = [
|
|
84
|
+
check_non_ascii_path,
|
|
85
|
+
check_homoglyph_in_path,
|
|
86
|
+
check_double_encoding,
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
HOSTNAME_CHECKS = [
|
|
90
|
+
check_non_ascii_hostname,
|
|
91
|
+
check_mixed_script_in_label,
|
|
92
|
+
check_userinfo_trick,
|
|
93
|
+
check_confusable_domain,
|
|
94
|
+
check_invalid_host_chars,
|
|
95
|
+
check_trailing_dot_whitespace,
|
|
96
|
+
check_non_standard_port,
|
|
97
|
+
check_lookalike_tld,
|
|
98
|
+
check_punycode_domain,
|
|
99
|
+
check_raw_ip_url,
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
ALL_CHECKS = (
|
|
103
|
+
COMMAND_SHAPE_CHECKS +
|
|
104
|
+
TERMINAL_CHECKS +
|
|
105
|
+
TRANSPORT_CHECKS +
|
|
106
|
+
ECOSYSTEM_CHECKS +
|
|
107
|
+
ENVIRONMENT_CHECKS +
|
|
108
|
+
PATH_CHECKS +
|
|
109
|
+
HOSTNAME_CHECKS
|
|
110
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from ..patterns import (
|
|
2
|
+
SOURCE_COMMANDS,
|
|
3
|
+
INTERPRETERS,
|
|
4
|
+
ARCHIVE_COMMANDS,
|
|
5
|
+
ARCHIVE_SENSITIVE_TARGETS,
|
|
6
|
+
DOTFILE_OVERWRITE_PATTERNS,
|
|
7
|
+
)
|
|
8
|
+
from .utils import split_commands
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _check_pipe_to_interpreter_single(command):
|
|
12
|
+
"""Check a single command (no && or ;) for pipe to interpreter."""
|
|
13
|
+
if "|" not in command:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
parts = command.split("|")
|
|
17
|
+
for i, part in enumerate(parts[1:], 1):
|
|
18
|
+
part_stripped = part.strip()
|
|
19
|
+
words = part_stripped.split()
|
|
20
|
+
if not words:
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
cmd = words[0].lower().rsplit("/", 1)[-1]
|
|
24
|
+
|
|
25
|
+
if cmd in INTERPRETERS:
|
|
26
|
+
source_part = parts[i-1].strip().split()[0] if parts[i-1].strip() else "unknown"
|
|
27
|
+
source_cmd = source_part.rsplit("/", 1)[-1].lower()
|
|
28
|
+
|
|
29
|
+
if source_cmd == "curl":
|
|
30
|
+
return ("curl_pipe_shell", f"curl output piped to {cmd}")
|
|
31
|
+
elif source_cmd == "wget":
|
|
32
|
+
return ("wget_pipe_shell", f"wget output piped to {cmd}")
|
|
33
|
+
elif source_cmd in SOURCE_COMMANDS:
|
|
34
|
+
return ("pipe_to_interpreter", f"{source_cmd} output piped to interpreter: {cmd}")
|
|
35
|
+
else:
|
|
36
|
+
return ("pipe_to_interpreter", f"Output piped to interpreter: {cmd}")
|
|
37
|
+
|
|
38
|
+
if cmd in ("sudo", "env"):
|
|
39
|
+
for word in words[1:]:
|
|
40
|
+
word_lower = word.lower().rsplit("/", 1)[-1]
|
|
41
|
+
if word_lower in INTERPRETERS:
|
|
42
|
+
return ("pipe_to_interpreter", f"Output piped to {cmd} {word_lower}")
|
|
43
|
+
if not word.startswith("-") and "=" not in word:
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_pipe_to_interpreter(command):
|
|
50
|
+
"""Check for pipe to interpreter, handling && and ; separators."""
|
|
51
|
+
for subcmd in split_commands(command):
|
|
52
|
+
result = _check_pipe_to_interpreter_single(subcmd)
|
|
53
|
+
if result:
|
|
54
|
+
return result
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check_dotfile_overwrite(command):
|
|
59
|
+
if "> /dev/null" in command:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
for subcmd in split_commands(command):
|
|
63
|
+
for pattern in DOTFILE_OVERWRITE_PATTERNS:
|
|
64
|
+
if pattern in subcmd:
|
|
65
|
+
return ("dotfile_overwrite", f"Redirect to dotfile detected: {pattern}")
|
|
66
|
+
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_archive_extract(command):
|
|
71
|
+
for subcmd in split_commands(command):
|
|
72
|
+
words = subcmd.split()
|
|
73
|
+
if not words:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
cmd = words[0].lower().rsplit("/", 1)[-1]
|
|
77
|
+
if cmd not in ARCHIVE_COMMANDS:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
for target in ARCHIVE_SENSITIVE_TARGETS:
|
|
81
|
+
if target in subcmd:
|
|
82
|
+
return ("archive_extract", f"Archive extraction to sensitive path: {target}")
|
|
83
|
+
|
|
84
|
+
return None
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from ..patterns import (
|
|
3
|
+
TRUSTED_DOCKER_REGISTRIES,
|
|
4
|
+
TRUSTED_PIP_HOSTS,
|
|
5
|
+
TRUSTED_NPM_HOSTS,
|
|
6
|
+
WEB3_INDICATORS,
|
|
7
|
+
POPULAR_REPOS,
|
|
8
|
+
)
|
|
9
|
+
from .utils import levenshtein
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_docker_untrusted_registry(command):
|
|
13
|
+
words = command.split()
|
|
14
|
+
if len(words) < 3:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
cmd = words[0].lower()
|
|
18
|
+
if cmd not in ("docker", "podman", "nerdctl"):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
subcmd = words[1].lower()
|
|
22
|
+
if subcmd not in ("pull", "run", "create"):
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
for word in words[2:]:
|
|
26
|
+
if word.startswith("-"):
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if "/" in word and ":" not in word.split("/")[0]:
|
|
30
|
+
registry = word.split("/")[0].lower()
|
|
31
|
+
if registry not in TRUSTED_DOCKER_REGISTRIES:
|
|
32
|
+
trusted = any(registry.endswith(f".{r}") for r in TRUSTED_DOCKER_REGISTRIES)
|
|
33
|
+
if not trusted and "." in registry:
|
|
34
|
+
return ("docker_untrusted_registry", f"Docker image from untrusted registry: {registry}")
|
|
35
|
+
break
|
|
36
|
+
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check_pip_url_install(command):
|
|
41
|
+
words = command.split()
|
|
42
|
+
if len(words) < 2:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
cmd = words[0].lower()
|
|
46
|
+
if cmd not in ("pip", "pip3"):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
if "install" not in [w.lower() for w in words]:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
urls = re.findall(r'https?://([^/\s]+)', command)
|
|
53
|
+
for host in urls:
|
|
54
|
+
host_lower = host.lower()
|
|
55
|
+
if host_lower not in TRUSTED_PIP_HOSTS and not host_lower.endswith(".pypi.org"):
|
|
56
|
+
return ("pip_url_install", f"pip install from non-PyPI source: {host}")
|
|
57
|
+
|
|
58
|
+
for i, word in enumerate(words):
|
|
59
|
+
if word in ("--index-url", "-i", "--extra-index-url"):
|
|
60
|
+
if i + 1 < len(words):
|
|
61
|
+
url = words[i + 1]
|
|
62
|
+
match = re.search(r'https?://([^/\s]+)', url)
|
|
63
|
+
if match:
|
|
64
|
+
host = match.group(1).lower()
|
|
65
|
+
if host not in TRUSTED_PIP_HOSTS and not host.endswith(".pypi.org"):
|
|
66
|
+
return ("pip_url_install", f"pip using non-PyPI index: {host}")
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def check_npm_url_install(command):
|
|
72
|
+
words = command.split()
|
|
73
|
+
if len(words) < 2:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
cmd = words[0].lower()
|
|
77
|
+
if cmd not in ("npm", "npx", "yarn", "pnpm"):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
if "install" not in [w.lower() for w in words] and "add" not in [w.lower() for w in words]:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
for word in words:
|
|
84
|
+
if ".tgz" in word or "/npm/" in word:
|
|
85
|
+
match = re.search(r'https?://([^/\s]+)', word)
|
|
86
|
+
if match:
|
|
87
|
+
host = match.group(1).lower()
|
|
88
|
+
if host not in TRUSTED_NPM_HOSTS and not host.endswith(".npmjs.org"):
|
|
89
|
+
return ("npm_url_install", f"npm install from non-registry source: {host}")
|
|
90
|
+
|
|
91
|
+
for i, word in enumerate(words):
|
|
92
|
+
if word == "--registry":
|
|
93
|
+
if i + 1 < len(words):
|
|
94
|
+
url = words[i + 1]
|
|
95
|
+
match = re.search(r'https?://([^/\s]+)', url)
|
|
96
|
+
if match:
|
|
97
|
+
host = match.group(1).lower()
|
|
98
|
+
if host not in TRUSTED_NPM_HOSTS and not host.endswith(".npmjs.org"):
|
|
99
|
+
return ("npm_url_install", f"npm using non-registry source: {host}")
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_web3_rpc(command):
|
|
105
|
+
if not any(ind in command for ind in ["/v1/", "/rpc", "/jsonrpc"]):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
command_lower = command.lower()
|
|
109
|
+
for indicator in WEB3_INDICATORS:
|
|
110
|
+
if indicator in command_lower:
|
|
111
|
+
return ("web3_rpc_endpoint", f"Web3 RPC endpoint detected: {indicator}")
|
|
112
|
+
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_web3_address(command):
|
|
117
|
+
if re.search(r'0x[0-9a-fA-F]{40}', command):
|
|
118
|
+
return ("web3_address_in_url", "Ethereum address detected in command")
|
|
119
|
+
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def check_git_typosquat(command):
|
|
124
|
+
words = command.split()
|
|
125
|
+
if len(words) < 2:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
cmd = words[0].lower()
|
|
129
|
+
if cmd != "git":
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
if "clone" not in [w.lower() for w in words]:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
urls = re.findall(r'(?:https?://|git@)([^/\s:]+)[:/]([^/\s]+)/([^/\s]+?)(?:\.git)?(?:\s|$)', command)
|
|
136
|
+
for host, owner, repo in urls:
|
|
137
|
+
host_lower = host.lower()
|
|
138
|
+
if host_lower not in ("github.com", "gitlab.com", "bitbucket.org"):
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
owner_lower = owner.lower()
|
|
142
|
+
repo_lower = repo.lower().rstrip('.git')
|
|
143
|
+
|
|
144
|
+
for pop_owner, pop_repo in POPULAR_REPOS:
|
|
145
|
+
po = pop_owner.lower()
|
|
146
|
+
pr = pop_repo.lower()
|
|
147
|
+
|
|
148
|
+
if owner_lower == po and levenshtein(repo_lower, pr) == 1:
|
|
149
|
+
return ("git_typosquat", f"Possible typosquat: {owner}/{repo} is 1 edit from {pop_owner}/{pop_repo}")
|
|
150
|
+
if repo_lower == pr and levenshtein(owner_lower, po) == 1:
|
|
151
|
+
return ("git_typosquat", f"Possible typosquat: {owner}/{repo} is 1 edit from {pop_owner}/{pop_repo}")
|
|
152
|
+
|
|
153
|
+
return None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ..patterns import PROXY_ENV_VARS
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def check_proxy_env_set(command):
|
|
5
|
+
for var in PROXY_ENV_VARS:
|
|
6
|
+
patterns = [
|
|
7
|
+
f"export {var}=",
|
|
8
|
+
f"{var}=",
|
|
9
|
+
f"set {var}=",
|
|
10
|
+
]
|
|
11
|
+
for pattern in patterns:
|
|
12
|
+
if pattern in command:
|
|
13
|
+
return ("proxy_env_set", f"Proxy environment variable being set: {var}")
|
|
14
|
+
|
|
15
|
+
return None
|