nxtsecure-openclaw 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/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/nxtsecure.mjs +212 -0
- package/package.json +39 -0
- package/skills/openclaw-security-audit/SKILL.md +104 -0
- package/skills/openclaw-security-audit/references/openclaw-security-audit.conf.example +36 -0
- package/skills/openclaw-security-audit/scripts/install_cron.sh +37 -0
- package/skills/openclaw-security-audit/scripts/openclaw_security_audit.sh +752 -0
- package/skills/openclaw-security-audit/scripts/openclaw_virustotal_check.sh +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 scorpion7slayer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Secure-your-openclaw
|
|
2
|
+
|
|
3
|
+
OpenClaw security audit skill with an integrated npm CLI.
|
|
4
|
+
|
|
5
|
+
Published and maintained by `scorpion7slayer`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g nxtsecure-openclaw
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then use:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
nxtsecure openclaw help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What this repository contains
|
|
20
|
+
|
|
21
|
+
- An OpenClaw skill in `skills/openclaw-security-audit/`
|
|
22
|
+
- A Node-based CLI exposed as `nxtsecure openclaw`
|
|
23
|
+
- Bash runners for audit, cron setup, and VirusTotal browser-assisted checks
|
|
24
|
+
|
|
25
|
+
## What the skill checks
|
|
26
|
+
|
|
27
|
+
- Firewall enabled
|
|
28
|
+
- `fail2ban` active
|
|
29
|
+
- SSH hardened with key-only auth and a non-default port
|
|
30
|
+
- Unexpected listening ports
|
|
31
|
+
- Docker container allowlisting
|
|
32
|
+
- Disk usage threshold
|
|
33
|
+
- Failed login attempts in the last 24 hours
|
|
34
|
+
- Automatic security package updates
|
|
35
|
+
- VirusTotal review for URLs and files without using a VirusTotal API key
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
1. Create a local config:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run nxtsecure -- openclaw config init --output ./openclaw-security-audit.conf
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. Review and edit the generated config.
|
|
46
|
+
|
|
47
|
+
3. Run the audit:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm run nxtsecure -- openclaw audit --config ./openclaw-security-audit.conf
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
4. Install the nightly cron job at 23:00:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm run nxtsecure -- openclaw cron install --log ~/openclaw-security-audit.log
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## CLI commands
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run nxtsecure -- openclaw help
|
|
63
|
+
npm run nxtsecure -- openclaw audit --config ./openclaw-security-audit.conf
|
|
64
|
+
npm run nxtsecure -- openclaw cron install --log ~/openclaw-security-audit.log
|
|
65
|
+
npm run nxtsecure -- openclaw vt url https://example.test
|
|
66
|
+
npm run nxtsecure -- openclaw vt file /path/to/sample.bin
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## VirusTotal mode
|
|
70
|
+
|
|
71
|
+
This repository intentionally avoids the VirusTotal API.
|
|
72
|
+
|
|
73
|
+
- URL checks are prepared for the public VirusTotal website through the OpenClaw browser tool.
|
|
74
|
+
- File checks compute the SHA-256 locally and prepare the public VirusTotal report URL.
|
|
75
|
+
- If a file is flagged, the agent must ask the user whether to keep or remove it.
|
|
76
|
+
|
|
77
|
+
## Repository layout
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
bin/nxtsecure.mjs
|
|
81
|
+
package.json
|
|
82
|
+
skills/openclaw-security-audit/SKILL.md
|
|
83
|
+
skills/openclaw-security-audit/references/openclaw-security-audit.conf.example
|
|
84
|
+
skills/openclaw-security-audit/scripts/install_cron.sh
|
|
85
|
+
skills/openclaw-security-audit/scripts/openclaw_security_audit.sh
|
|
86
|
+
skills/openclaw-security-audit/scripts/openclaw_virustotal_check.sh
|
|
87
|
+
```
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const rootDir = resolve(currentDir, '..');
|
|
10
|
+
const skillDir = resolve(rootDir, 'skills', 'openclaw-security-audit');
|
|
11
|
+
const scriptsDir = resolve(skillDir, 'scripts');
|
|
12
|
+
const referencesDir = resolve(skillDir, 'references');
|
|
13
|
+
|
|
14
|
+
const paths = {
|
|
15
|
+
audit: resolve(scriptsDir, 'openclaw_security_audit.sh'),
|
|
16
|
+
cron: resolve(scriptsDir, 'install_cron.sh'),
|
|
17
|
+
vt: resolve(scriptsDir, 'openclaw_virustotal_check.sh'),
|
|
18
|
+
configExample: resolve(referencesDir, 'openclaw-security-audit.conf.example')
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function printHelp() {
|
|
22
|
+
console.log(`nxtsecure openclaw
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
nxtsecure help
|
|
26
|
+
nxtsecure openclaw help
|
|
27
|
+
nxtsecure openclaw audit [--config PATH]
|
|
28
|
+
nxtsecure openclaw cron install [--log PATH]
|
|
29
|
+
nxtsecure openclaw vt url <url> [--allow-uploads]
|
|
30
|
+
nxtsecure openclaw vt file <path> [--allow-uploads]
|
|
31
|
+
nxtsecure openclaw config init [--output PATH] [--force]
|
|
32
|
+
nxtsecure openclaw paths
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
nxtsecure openclaw config init --output ./openclaw-security-audit.conf
|
|
36
|
+
nxtsecure openclaw audit --config ./openclaw-security-audit.conf
|
|
37
|
+
nxtsecure openclaw cron install --log ~/openclaw-security-audit.log
|
|
38
|
+
nxtsecure openclaw vt url https://example.test
|
|
39
|
+
nxtsecure openclaw vt file /tmp/sample.bin --allow-uploads
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fail(message, exitCode = 1) {
|
|
44
|
+
console.error(`[ERROR] ${message}`);
|
|
45
|
+
process.exit(exitCode);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runBashScript(scriptPath, args = [], env = {}) {
|
|
49
|
+
if (!existsSync(scriptPath)) {
|
|
50
|
+
fail(`Missing script: ${scriptPath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = spawnSync('bash', [scriptPath, ...args], {
|
|
54
|
+
cwd: rootDir,
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
env: { ...process.env, ...env }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.error) {
|
|
60
|
+
fail(result.error.message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process.exit(result.status ?? 1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function takeOption(argv, optionName) {
|
|
67
|
+
const index = argv.indexOf(optionName);
|
|
68
|
+
if (index === -1) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (index === argv.length - 1) {
|
|
72
|
+
fail(`Missing value for ${optionName}`);
|
|
73
|
+
}
|
|
74
|
+
return argv[index + 1];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasFlag(argv, flagName) {
|
|
78
|
+
return argv.includes(flagName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function withoutOption(argv, optionName) {
|
|
82
|
+
const index = argv.indexOf(optionName);
|
|
83
|
+
if (index === -1) {
|
|
84
|
+
return argv.slice();
|
|
85
|
+
}
|
|
86
|
+
const clone = argv.slice();
|
|
87
|
+
clone.splice(index, 2);
|
|
88
|
+
return clone;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function withoutFlag(argv, flagName) {
|
|
92
|
+
return argv.filter((item) => item !== flagName);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function commandAudit(argv) {
|
|
96
|
+
const configPath = takeOption(argv, '--config');
|
|
97
|
+
const args = configPath ? withoutOption(argv, '--config') : argv.slice();
|
|
98
|
+
if (args.length !== 0) {
|
|
99
|
+
fail(`Unknown audit arguments: ${args.join(' ')}`);
|
|
100
|
+
}
|
|
101
|
+
runBashScript(paths.audit, [], configPath ? { OPENCLAW_AUDIT_CONFIG: resolve(rootDir, configPath) } : {});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function commandCron(argv) {
|
|
105
|
+
if (argv[0] !== 'install') {
|
|
106
|
+
fail('Usage: nxtsecure openclaw cron install [--log PATH]');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const logPath = takeOption(argv, '--log');
|
|
110
|
+
const args = logPath ? withoutOption(argv.slice(1), '--log') : argv.slice(1);
|
|
111
|
+
if (args.length !== 0) {
|
|
112
|
+
fail(`Unknown cron arguments: ${args.join(' ')}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
runBashScript(paths.cron, [], logPath ? { OPENCLAW_AUDIT_LOG: logPath } : {});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function commandVirusTotal(argv) {
|
|
119
|
+
const allowUploads = hasFlag(argv, '--allow-uploads');
|
|
120
|
+
const cleaned = allowUploads ? withoutFlag(argv, '--allow-uploads') : argv.slice();
|
|
121
|
+
const mode = cleaned[0];
|
|
122
|
+
const value = cleaned[1];
|
|
123
|
+
|
|
124
|
+
if (!mode || !value || cleaned.length !== 2 || !['url', 'file'].includes(mode)) {
|
|
125
|
+
fail('Usage: nxtsecure openclaw vt url <url> [--allow-uploads] | nxtsecure openclaw vt file <path> [--allow-uploads]');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const scriptArgs = mode === 'url' ? ['--url', value] : ['--file', value];
|
|
129
|
+
runBashScript(paths.vt, scriptArgs, {
|
|
130
|
+
VIRUSTOTAL_ALLOW_UPLOADS: allowUploads ? '1' : '0'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function commandConfig(argv) {
|
|
135
|
+
if (argv[0] !== 'init') {
|
|
136
|
+
fail('Usage: nxtsecure openclaw config init [--output PATH] [--force]');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const force = hasFlag(argv, '--force');
|
|
140
|
+
const output = takeOption(argv, '--output') ?? './openclaw-security-audit.conf';
|
|
141
|
+
const cleaned = force ? withoutFlag(argv.slice(1), '--force') : argv.slice(1);
|
|
142
|
+
const finalArgs = output ? withoutOption(cleaned, '--output') : cleaned;
|
|
143
|
+
if (finalArgs.length !== 0) {
|
|
144
|
+
fail(`Unknown config arguments: ${finalArgs.join(' ')}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const targetPath = resolve(rootDir, output);
|
|
148
|
+
if (existsSync(targetPath) && !force) {
|
|
149
|
+
fail(`Config already exists: ${targetPath}. Use --force to overwrite.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
153
|
+
copyFileSync(paths.configExample, targetPath);
|
|
154
|
+
console.log(`Created config: ${targetPath}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function commandPaths(argv) {
|
|
158
|
+
if (argv.length !== 0) {
|
|
159
|
+
fail(`Unknown paths arguments: ${argv.join(' ')}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`root=${rootDir}`);
|
|
163
|
+
console.log(`skill=${skillDir}`);
|
|
164
|
+
console.log(`audit=${paths.audit}`);
|
|
165
|
+
console.log(`cron=${paths.cron}`);
|
|
166
|
+
console.log(`vt=${paths.vt}`);
|
|
167
|
+
console.log(`configExample=${paths.configExample}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function runOpenClaw(argv) {
|
|
171
|
+
const [command = 'help', ...rest] = argv;
|
|
172
|
+
|
|
173
|
+
switch (command) {
|
|
174
|
+
case 'help':
|
|
175
|
+
case '--help':
|
|
176
|
+
case '-h':
|
|
177
|
+
printHelp();
|
|
178
|
+
break;
|
|
179
|
+
case 'audit':
|
|
180
|
+
commandAudit(rest);
|
|
181
|
+
break;
|
|
182
|
+
case 'cron':
|
|
183
|
+
commandCron(rest);
|
|
184
|
+
break;
|
|
185
|
+
case 'vt':
|
|
186
|
+
commandVirusTotal(rest);
|
|
187
|
+
break;
|
|
188
|
+
case 'config':
|
|
189
|
+
commandConfig(rest);
|
|
190
|
+
break;
|
|
191
|
+
case 'paths':
|
|
192
|
+
commandPaths(rest);
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
fail(`Unknown openclaw command: ${command}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const [namespace = 'help', ...rest] = process.argv.slice(2);
|
|
200
|
+
|
|
201
|
+
switch (namespace) {
|
|
202
|
+
case 'help':
|
|
203
|
+
case '--help':
|
|
204
|
+
case '-h':
|
|
205
|
+
printHelp();
|
|
206
|
+
break;
|
|
207
|
+
case 'openclaw':
|
|
208
|
+
runOpenClaw(rest);
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
fail(`Unknown namespace: ${namespace}. Expected: openclaw`);
|
|
212
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nxtsecure-openclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "npm CLI wrapper for the OpenClaw security audit skill",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nxtsecure": "bin/nxtsecure.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"skills",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/scorpion7slayer/Secure-your-openclaw.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/scorpion7slayer/Secure-your-openclaw#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/scorpion7slayer/Secure-your-openclaw/issues"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"openclaw",
|
|
26
|
+
"security",
|
|
27
|
+
"audit",
|
|
28
|
+
"cli",
|
|
29
|
+
"virustotal",
|
|
30
|
+
"ssh"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"nxtsecure": "node ./bin/nxtsecure.mjs",
|
|
34
|
+
"test": "node ./bin/nxtsecure.mjs help"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: openclaw-security-audit
|
|
3
|
+
description: Use when auditing and remediating an OpenClaw Linux host with a nightly 23:00 security run. Covers firewall status, fail2ban bans, SSH hardening with key-only auth and a non-default port, unexpected listening ports, Docker container allowlisting, disk usage under 80%, failed login attempts in the last 24 hours, automatic security package updates, VirusTotal browser-based checks for URLs and files without API keys, and installing the cron entry.
|
|
4
|
+
homepage: https://openclaw.ai/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# OpenClaw Security Audit
|
|
8
|
+
|
|
9
|
+
Original requested prompt, preserved verbatim:
|
|
10
|
+
"Effectuez un audit de sécurité tous les soirs à 23h faite un cron."
|
|
11
|
+
|
|
12
|
+
Use this skill when the user wants a repeatable OpenClaw host security audit, a nightly cron job, or immediate remediation of common hardening gaps.
|
|
13
|
+
|
|
14
|
+
## Workflow
|
|
15
|
+
|
|
16
|
+
1. Prefer the npm CLI in `{baseDir}/../../bin/nxtsecure.mjs` to keep the agent workflow stable.
|
|
17
|
+
2. From the repository root, create the local configuration with `npm run nxtsecure -- openclaw config init --output ./openclaw-security-audit.conf` or copy `{baseDir}/references/openclaw-security-audit.conf.example`.
|
|
18
|
+
3. Run `npm run nxtsecure -- openclaw audit --config ./openclaw-security-audit.conf` to execute the audit and remediation workflow.
|
|
19
|
+
4. Install the nightly 23:00 cron entry with `npm run nxtsecure -- openclaw cron install --log ~/openclaw-security-audit.log`.
|
|
20
|
+
5. If every check passes, print exactly `audit de sécurité réussi`.
|
|
21
|
+
6. If a check fails, explain the issue, attempt remediation immediately, and rerun the relevant verification.
|
|
22
|
+
|
|
23
|
+
## Checks
|
|
24
|
+
|
|
25
|
+
The audit must verify:
|
|
26
|
+
|
|
27
|
+
1. Firewall enabled.
|
|
28
|
+
2. `fail2ban` active and total banned IP count collected.
|
|
29
|
+
3. If SSH is used, password authentication must be disabled, public key authentication must be available, and the SSH service must not listen on port `22`.
|
|
30
|
+
4. Unexpected listening ports identified and, when configured, blocked.
|
|
31
|
+
5. Docker containers reviewed when Docker is present, with unexpected containers stopped only when an allowlist is configured.
|
|
32
|
+
6. Disk usage below `80%` on persistent filesystems.
|
|
33
|
+
7. Failed login attempts during the last 24 hours.
|
|
34
|
+
8. Automatic security package updates enabled on the host.
|
|
35
|
+
9. If VirusTotal is configured, URLs and files in scope must be checked before being trusted.
|
|
36
|
+
|
|
37
|
+
## SSH hardening guidance
|
|
38
|
+
|
|
39
|
+
When SSH is enabled, the agent must help the user migrate safely instead of changing access blindly.
|
|
40
|
+
|
|
41
|
+
1. Explain the goal: SSH on a non-default port and key-only authentication.
|
|
42
|
+
2. Ask or infer the target SSH port from configuration. Use `2222` only as a fallback example, not a forced default.
|
|
43
|
+
3. Help the user generate a key pair if needed:
|
|
44
|
+
`ssh-keygen -t ed25519 -C "openclaw-admin"`
|
|
45
|
+
4. Help the user install the public key on the server:
|
|
46
|
+
`ssh-copy-id -p <new-port> <user>@<host>`
|
|
47
|
+
or append the public key to `~/.ssh/authorized_keys` with correct permissions.
|
|
48
|
+
5. Update SSH to use the chosen non-default port and disable password authentication.
|
|
49
|
+
6. Make sure the firewall allows the new SSH port before reloading SSH.
|
|
50
|
+
7. Tell the user to open a second terminal and verify:
|
|
51
|
+
`ssh -p <new-port> <user>@<host>`
|
|
52
|
+
8. Only after the new key-based login works, remove any temporary legacy access and confirm the hardening is complete.
|
|
53
|
+
|
|
54
|
+
If the agent cannot verify that key-based access on the new port works, it must explain the exact manual steps still required and avoid risky lockout actions.
|
|
55
|
+
|
|
56
|
+
## VirusTotal guidance
|
|
57
|
+
|
|
58
|
+
When the user wants file or link reputation checks, the agent must use VirusTotal without an API key:
|
|
59
|
+
|
|
60
|
+
1. Use the OpenClaw `browser` tool, not the VirusTotal API.
|
|
61
|
+
2. Ensure the OpenClaw browser tool is enabled before starting the workflow.
|
|
62
|
+
3. For files, compute the SHA-256 locally first and prefer the public report page for an existing report.
|
|
63
|
+
4. Only upload a file through the VirusTotal website when the user has explicitly allowed it, because website uploads may disclose the sample outside the organization.
|
|
64
|
+
5. For URLs, open the public VirusTotal URL page in the browser tool and submit the URL for analysis through the web interface.
|
|
65
|
+
6. If a file or URL is malicious, explain the verdict. For files, ask the user whether to keep or remove the file. For URLs, recommend blocking the URL or domain.
|
|
66
|
+
7. If an item is suspicious, explain the risk and require explicit user confirmation before trusting it.
|
|
67
|
+
8. For nightly automation, treat VirusTotal as browser-assisted review.
|
|
68
|
+
9. If VirusTotal flags a file as malicious or suspicious, the agent must ask the user whether to keep or remove the file. The user always decides.
|
|
69
|
+
10. Do not claim that a URL or file was cleared automatically when the agent has only prepared the VirusTotal browser workflow and not inspected the result page.
|
|
70
|
+
|
|
71
|
+
Use the bundled helper:
|
|
72
|
+
|
|
73
|
+
- `npm run nxtsecure -- openclaw vt url https://example.test`
|
|
74
|
+
- `npm run nxtsecure -- openclaw vt file /path/to/sample.bin`
|
|
75
|
+
- fallback: `{baseDir}/scripts/openclaw_virustotal_check.sh --url https://example.test`
|
|
76
|
+
- fallback: `{baseDir}/scripts/openclaw_virustotal_check.sh --file /path/to/sample.bin`
|
|
77
|
+
|
|
78
|
+
OpenClaw browser flow:
|
|
79
|
+
|
|
80
|
+
1. `browser.start`
|
|
81
|
+
2. `browser.open` or `browser.navigate` to `https://www.virustotal.com/gui/home/url` for URLs
|
|
82
|
+
3. `browser.open` or `browser.navigate` to `https://www.virustotal.com/gui/home/upload` for files
|
|
83
|
+
4. Use `browser.snapshot` and `browser.act` to type, upload, and inspect detection results
|
|
84
|
+
|
|
85
|
+
## Operational notes
|
|
86
|
+
|
|
87
|
+
- Run the audit as `root` when possible. Some remediations require privileged access.
|
|
88
|
+
- Adjust expected ports and allowed Docker containers before enabling strict enforcement.
|
|
89
|
+
- The bundled script prefers `ufw`, then `firewalld`, then a non-empty `nftables` ruleset for firewall detection.
|
|
90
|
+
- The script uses `sshd -T` when available and falls back to SSH config files.
|
|
91
|
+
- The bundled SSH policy expects a non-default port whenever SSH is enabled. Port `22` is treated as non-compliant.
|
|
92
|
+
- The audit should enable automatic security updates when supported by the distribution, such as `unattended-upgrades` on Debian or Ubuntu and `dnf-automatic` on RPM-based hosts.
|
|
93
|
+
- Failed logins are collected from `journalctl`, `lastb`, or `/var/log/auth.log`, depending on what the host exposes.
|
|
94
|
+
- VirusTotal checks in this skill are intentionally API-free and rely on the public website plus the OpenClaw browser tool.
|
|
95
|
+
- The nightly cron line installed by the helper is `0 23 * * *`.
|
|
96
|
+
|
|
97
|
+
## Files
|
|
98
|
+
|
|
99
|
+
- `{baseDir}/../../package.json`: npm package definition for the `nxtsecure openclaw` CLI.
|
|
100
|
+
- `{baseDir}/../../bin/nxtsecure.mjs`: npm CLI entrypoint for audit, cron, VirusTotal, and config init.
|
|
101
|
+
- `{baseDir}/scripts/openclaw_security_audit.sh`: audit and remediation runner.
|
|
102
|
+
- `{baseDir}/scripts/openclaw_virustotal_check.sh`: VirusTotal URL and file reputation helper.
|
|
103
|
+
- `{baseDir}/scripts/install_cron.sh`: idempotent cron installer for `23:00` every day.
|
|
104
|
+
- `{baseDir}/references/openclaw-security-audit.conf.example`: baseline configuration template.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copy this file to:
|
|
2
|
+
# - /etc/openclaw-security-audit.conf
|
|
3
|
+
# or point the audit to it with OPENCLAW_AUDIT_CONFIG=/path/to/file
|
|
4
|
+
|
|
5
|
+
# Expected listening ports. Do not keep 22 here if SSH is enabled.
|
|
6
|
+
# Include the chosen SSH_EXPECTED_PORT instead.
|
|
7
|
+
EXPECTED_TCP_PORTS="80 443 2222"
|
|
8
|
+
EXPECTED_UDP_PORTS=""
|
|
9
|
+
|
|
10
|
+
# SSH policy. If SSH is enabled, it must not use port 22.
|
|
11
|
+
# Set SSH_EXPECTED_PORT to the chosen non-default port, for example 2222.
|
|
12
|
+
SSH_EXPECTED_PORT="2222"
|
|
13
|
+
SSH_PORT_IN_USE=1
|
|
14
|
+
SSH_REQUIRE_KEYS_ONLY=1
|
|
15
|
+
ENABLE_SSH_PORT_REMEDIATION=0
|
|
16
|
+
|
|
17
|
+
# VirusTotal integration for URLs and files, without API keys.
|
|
18
|
+
# The agent should use the public VirusTotal website through the OpenClaw browser tool.
|
|
19
|
+
VIRUSTOTAL_ENABLED=0
|
|
20
|
+
VIRUSTOTAL_ALLOW_UPLOADS=0
|
|
21
|
+
VIRUSTOTAL_URLS=""
|
|
22
|
+
VIRUSTOTAL_FILES=""
|
|
23
|
+
|
|
24
|
+
# List allowed Docker container names, separated by spaces.
|
|
25
|
+
# Leave empty to force the audit to report unmanaged running containers.
|
|
26
|
+
ALLOWED_DOCKER_CONTAINERS=""
|
|
27
|
+
|
|
28
|
+
# Security thresholds.
|
|
29
|
+
DISK_THRESHOLD_PERCENT=80
|
|
30
|
+
MAX_FAILED_LOGIN_ATTEMPTS=25
|
|
31
|
+
AUTO_SECURITY_UPDATES_REQUIRED=1
|
|
32
|
+
|
|
33
|
+
# Remediation controls.
|
|
34
|
+
AUTO_REMEDIATE=1
|
|
35
|
+
REMEDIATE_UNEXPECTED_PORTS=0
|
|
36
|
+
ALLOW_DOCKER_PRUNE=0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
AUDIT_SCRIPT="${SCRIPT_DIR}/openclaw_security_audit.sh"
|
|
7
|
+
LOG_PATH="${OPENCLAW_AUDIT_LOG:-${HOME}/openclaw-security-audit.log}"
|
|
8
|
+
|
|
9
|
+
if [[ ! -x "${AUDIT_SCRIPT}" ]]; then
|
|
10
|
+
chmod +x "${AUDIT_SCRIPT}"
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
START_MARKER="# OPENCLAW_SECURITY_AUDIT_START"
|
|
14
|
+
END_MARKER="# OPENCLAW_SECURITY_AUDIT_END"
|
|
15
|
+
CRON_LINE="0 23 * * * /bin/bash ${AUDIT_SCRIPT} >> ${LOG_PATH} 2>&1"
|
|
16
|
+
|
|
17
|
+
CURRENT_CRONTAB="$(mktemp)"
|
|
18
|
+
UPDATED_CRONTAB="$(mktemp)"
|
|
19
|
+
|
|
20
|
+
trap 'rm -f "${CURRENT_CRONTAB}" "${UPDATED_CRONTAB}"' EXIT
|
|
21
|
+
|
|
22
|
+
crontab -l 2>/dev/null > "${CURRENT_CRONTAB}" || true
|
|
23
|
+
awk -v start="${START_MARKER}" -v end="${END_MARKER}" '
|
|
24
|
+
$0 == start { skip = 1; next }
|
|
25
|
+
$0 == end { skip = 0; next }
|
|
26
|
+
skip != 1 { print }
|
|
27
|
+
' "${CURRENT_CRONTAB}" > "${UPDATED_CRONTAB}"
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
cat "${UPDATED_CRONTAB}"
|
|
31
|
+
printf '%s\n' "${START_MARKER}"
|
|
32
|
+
printf '%s\n' "${CRON_LINE}"
|
|
33
|
+
printf '%s\n' "${END_MARKER}"
|
|
34
|
+
} | crontab -
|
|
35
|
+
|
|
36
|
+
printf 'Installed nightly cron at 23:00.\n'
|
|
37
|
+
printf 'Log file: %s\n' "${LOG_PATH}"
|
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
7
|
+
|
|
8
|
+
CONFIG_FILE="${OPENCLAW_AUDIT_CONFIG:-}"
|
|
9
|
+
if [[ -z "${CONFIG_FILE}" ]]; then
|
|
10
|
+
for candidate in \
|
|
11
|
+
"/etc/openclaw-security-audit.conf" \
|
|
12
|
+
"${SKILL_DIR}/references/openclaw-security-audit.conf" \
|
|
13
|
+
"${SKILL_DIR}/references/openclaw-security-audit.conf.example"
|
|
14
|
+
do
|
|
15
|
+
if [[ -f "${candidate}" ]]; then
|
|
16
|
+
CONFIG_FILE="${candidate}"
|
|
17
|
+
break
|
|
18
|
+
fi
|
|
19
|
+
done
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
EXPECTED_TCP_PORTS="80 443 2222"
|
|
23
|
+
EXPECTED_UDP_PORTS=""
|
|
24
|
+
SSH_EXPECTED_PORT="2222"
|
|
25
|
+
SSH_PORT_IN_USE=1
|
|
26
|
+
SSH_REQUIRE_KEYS_ONLY=1
|
|
27
|
+
ENABLE_SSH_PORT_REMEDIATION=0
|
|
28
|
+
VIRUSTOTAL_ENABLED=0
|
|
29
|
+
VIRUSTOTAL_ALLOW_UPLOADS=0
|
|
30
|
+
VIRUSTOTAL_URLS=""
|
|
31
|
+
VIRUSTOTAL_FILES=""
|
|
32
|
+
ALLOWED_DOCKER_CONTAINERS=""
|
|
33
|
+
DISK_THRESHOLD_PERCENT=80
|
|
34
|
+
MAX_FAILED_LOGIN_ATTEMPTS=25
|
|
35
|
+
AUTO_SECURITY_UPDATES_REQUIRED=1
|
|
36
|
+
AUTO_REMEDIATE=1
|
|
37
|
+
REMEDIATE_UNEXPECTED_PORTS=0
|
|
38
|
+
ALLOW_DOCKER_PRUNE=0
|
|
39
|
+
|
|
40
|
+
if [[ -n "${CONFIG_FILE}" && -f "${CONFIG_FILE}" ]]; then
|
|
41
|
+
# shellcheck disable=SC1090
|
|
42
|
+
source "${CONFIG_FILE}"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
declare -a ISSUES=()
|
|
46
|
+
declare -a ACTIONS=()
|
|
47
|
+
declare -a WARNINGS=()
|
|
48
|
+
|
|
49
|
+
log() {
|
|
50
|
+
printf '[INFO] %s\n' "$*"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
warn() {
|
|
54
|
+
WARNINGS+=("$*")
|
|
55
|
+
printf '[WARN] %s\n' "$*"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
issue() {
|
|
59
|
+
ISSUES+=("$*")
|
|
60
|
+
printf '[FAIL] %s\n' "$*"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
action() {
|
|
64
|
+
ACTIONS+=("$*")
|
|
65
|
+
printf '[FIX] %s\n' "$*"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
command_exists() {
|
|
69
|
+
command -v "$1" >/dev/null 2>&1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
is_root() {
|
|
73
|
+
[[ "${EUID}" -eq 0 ]]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
run_privileged() {
|
|
77
|
+
if is_root; then
|
|
78
|
+
"$@"
|
|
79
|
+
elif command_exists sudo && sudo -n true >/dev/null 2>&1; then
|
|
80
|
+
sudo "$@"
|
|
81
|
+
else
|
|
82
|
+
return 126
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
contains_word() {
|
|
87
|
+
local needle="$1"
|
|
88
|
+
local haystack="$2"
|
|
89
|
+
local item
|
|
90
|
+
for item in ${haystack}; do
|
|
91
|
+
if [[ "${item}" == "${needle}" ]]; then
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
return 1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if [[ "${SSH_PORT_IN_USE}" -eq 1 ]] && ! contains_word "${SSH_EXPECTED_PORT}" "${EXPECTED_TCP_PORTS}"; then
|
|
99
|
+
EXPECTED_TCP_PORTS="${EXPECTED_TCP_PORTS} ${SSH_EXPECTED_PORT}"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
service_is_active() {
|
|
103
|
+
local service="$1"
|
|
104
|
+
command_exists systemctl && systemctl is-active --quiet "${service}"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
safe_systemctl_enable_now() {
|
|
108
|
+
local service="$1"
|
|
109
|
+
if run_privileged systemctl enable --now "${service}" >/dev/null 2>&1; then
|
|
110
|
+
action "Enabled ${service}."
|
|
111
|
+
return 0
|
|
112
|
+
fi
|
|
113
|
+
return 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
detect_firewall_backend() {
|
|
117
|
+
if command_exists ufw; then
|
|
118
|
+
echo "ufw"
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
if command_exists firewall-cmd; then
|
|
122
|
+
echo "firewalld"
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
if command_exists nft; then
|
|
126
|
+
echo "nftables"
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
echo "none"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
check_firewall() {
|
|
133
|
+
local backend
|
|
134
|
+
backend="$(detect_firewall_backend)"
|
|
135
|
+
|
|
136
|
+
case "${backend}" in
|
|
137
|
+
ufw)
|
|
138
|
+
if ufw status 2>/dev/null | grep -q "^Status: active"; then
|
|
139
|
+
log "Firewall active via ufw."
|
|
140
|
+
elif [[ "${AUTO_REMEDIATE}" -eq 1 ]] && run_privileged ufw --force enable >/dev/null 2>&1; then
|
|
141
|
+
action "Enabled ufw."
|
|
142
|
+
else
|
|
143
|
+
issue "ufw is installed but not active."
|
|
144
|
+
fi
|
|
145
|
+
;;
|
|
146
|
+
firewalld)
|
|
147
|
+
if service_is_active firewalld; then
|
|
148
|
+
log "Firewall active via firewalld."
|
|
149
|
+
elif [[ "${AUTO_REMEDIATE}" -eq 1 ]] && safe_systemctl_enable_now firewalld; then
|
|
150
|
+
:
|
|
151
|
+
else
|
|
152
|
+
issue "firewalld is installed but not active."
|
|
153
|
+
fi
|
|
154
|
+
;;
|
|
155
|
+
nftables)
|
|
156
|
+
if nft list ruleset 2>/dev/null | grep -q "table"; then
|
|
157
|
+
log "Firewall appears active via nftables."
|
|
158
|
+
else
|
|
159
|
+
issue "nftables is present but no active ruleset was found."
|
|
160
|
+
fi
|
|
161
|
+
;;
|
|
162
|
+
*)
|
|
163
|
+
issue "No supported firewall manager was found."
|
|
164
|
+
;;
|
|
165
|
+
esac
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fail2ban_banned_total() {
|
|
169
|
+
local total=0
|
|
170
|
+
local jail
|
|
171
|
+
local jails
|
|
172
|
+
|
|
173
|
+
if ! command_exists fail2ban-client; then
|
|
174
|
+
echo "0"
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
jails="$(fail2ban-client status 2>/dev/null | sed -n 's/.*Jail list:[[:space:]]*//p' | tr ',' ' ')"
|
|
179
|
+
if [[ -z "${jails}" ]]; then
|
|
180
|
+
echo "0"
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
for jail in ${jails}; do
|
|
185
|
+
local count
|
|
186
|
+
count="$(fail2ban-client status "${jail}" 2>/dev/null | sed -n 's/.*Total banned:[[:space:]]*//p' | tail -n1)"
|
|
187
|
+
if [[ "${count}" =~ ^[0-9]+$ ]]; then
|
|
188
|
+
total=$((total + count))
|
|
189
|
+
fi
|
|
190
|
+
done
|
|
191
|
+
|
|
192
|
+
echo "${total}"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
check_fail2ban() {
|
|
196
|
+
if ! command_exists fail2ban-client && ! service_is_active fail2ban; then
|
|
197
|
+
issue "fail2ban is not installed or not available."
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
if service_is_active fail2ban; then
|
|
202
|
+
log "fail2ban is active. Total banned IPs: $(fail2ban_banned_total)."
|
|
203
|
+
elif [[ "${AUTO_REMEDIATE}" -eq 1 ]] && safe_systemctl_enable_now fail2ban; then
|
|
204
|
+
log "fail2ban is active. Total banned IPs: $(fail2ban_banned_total)."
|
|
205
|
+
else
|
|
206
|
+
issue "fail2ban is installed but inactive."
|
|
207
|
+
fi
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ssh_effective_value() {
|
|
211
|
+
local key="$1"
|
|
212
|
+
if command_exists sshd; then
|
|
213
|
+
sshd -T 2>/dev/null | awk -v target="${key}" '$1 == target { print $2; exit }'
|
|
214
|
+
return 0
|
|
215
|
+
fi
|
|
216
|
+
awk -v target="${key}" '
|
|
217
|
+
BEGIN { IGNORECASE=1 }
|
|
218
|
+
$1 == target { value = $2 }
|
|
219
|
+
END { if (value != "") print tolower(value) }
|
|
220
|
+
' /etc/ssh/sshd_config 2>/dev/null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
remediate_ssh_password_auth() {
|
|
224
|
+
local drop_in="/etc/ssh/sshd_config.d/99-openclaw-hardening.conf"
|
|
225
|
+
local tmp_file
|
|
226
|
+
|
|
227
|
+
tmp_file="$(mktemp)"
|
|
228
|
+
cat >"${tmp_file}" <<'EOF'
|
|
229
|
+
PasswordAuthentication no
|
|
230
|
+
KbdInteractiveAuthentication no
|
|
231
|
+
ChallengeResponseAuthentication no
|
|
232
|
+
EOF
|
|
233
|
+
|
|
234
|
+
if run_privileged mkdir -p /etc/ssh/sshd_config.d \
|
|
235
|
+
&& run_privileged install -m 0644 "${tmp_file}" "${drop_in}" \
|
|
236
|
+
&& run_privileged sshd -t \
|
|
237
|
+
&& (run_privileged systemctl reload sshd >/dev/null 2>&1 || run_privileged systemctl reload ssh >/dev/null 2>&1); then
|
|
238
|
+
rm -f "${tmp_file}"
|
|
239
|
+
action "Disabled SSH password authentication with ${drop_in}."
|
|
240
|
+
return 0
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
rm -f "${tmp_file}"
|
|
244
|
+
return 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ssh_effective_ports() {
|
|
248
|
+
if command_exists sshd; then
|
|
249
|
+
sshd -T 2>/dev/null | awk '$1 == "port" { print $2 }'
|
|
250
|
+
return 0
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
awk '
|
|
254
|
+
BEGIN { IGNORECASE=1 }
|
|
255
|
+
$1 == "Port" { print $2 }
|
|
256
|
+
' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
ssh_key_auth_enabled() {
|
|
260
|
+
local key_auth
|
|
261
|
+
key_auth="$(ssh_effective_value pubkeyauthentication)"
|
|
262
|
+
[[ -z "${key_auth}" || "${key_auth}" == "yes" ]]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
remediate_ssh_port() {
|
|
266
|
+
local desired_port="$1"
|
|
267
|
+
local drop_in="/etc/ssh/sshd_config.d/98-openclaw-port.conf"
|
|
268
|
+
local tmp_file
|
|
269
|
+
|
|
270
|
+
if [[ -z "${desired_port}" || "${desired_port}" == "22" ]]; then
|
|
271
|
+
return 1
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
tmp_file="$(mktemp)"
|
|
275
|
+
cat >"${tmp_file}" <<EOF
|
|
276
|
+
Port ${desired_port}
|
|
277
|
+
EOF
|
|
278
|
+
|
|
279
|
+
if ! run_privileged mkdir -p /etc/ssh/sshd_config.d; then
|
|
280
|
+
rm -f "${tmp_file}"
|
|
281
|
+
return 1
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
if ! run_privileged install -m 0644 "${tmp_file}" "${drop_in}"; then
|
|
285
|
+
rm -f "${tmp_file}"
|
|
286
|
+
return 1
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
rm -f "${tmp_file}"
|
|
290
|
+
|
|
291
|
+
if ! run_privileged sshd -t; then
|
|
292
|
+
return 1
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
if ! run_privileged systemctl reload sshd >/dev/null 2>&1 \
|
|
296
|
+
&& ! run_privileged systemctl reload ssh >/dev/null 2>&1; then
|
|
297
|
+
return 1
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
action "Configured SSH to listen on port ${desired_port}. Verify access with: ssh -p ${desired_port} <user>@<host>"
|
|
301
|
+
return 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
check_ssh() {
|
|
305
|
+
local password_auth
|
|
306
|
+
local ports
|
|
307
|
+
local port_ok=1
|
|
308
|
+
password_auth="$(ssh_effective_value passwordauthentication)"
|
|
309
|
+
|
|
310
|
+
if [[ "${SSH_REQUIRE_KEYS_ONLY}" -eq 1 ]]; then
|
|
311
|
+
if [[ "${password_auth}" != "no" ]]; then
|
|
312
|
+
if [[ "${AUTO_REMEDIATE}" -eq 1 ]] && remediate_ssh_password_auth; then
|
|
313
|
+
password_auth="$(ssh_effective_value passwordauthentication)"
|
|
314
|
+
fi
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
if [[ "${password_auth}" != "no" ]]; then
|
|
318
|
+
issue "SSH password authentication is still enabled."
|
|
319
|
+
elif ! ssh_key_auth_enabled; then
|
|
320
|
+
issue "SSH public key authentication is not enabled."
|
|
321
|
+
else
|
|
322
|
+
log "SSH is configured for key-based authentication only."
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
else
|
|
326
|
+
log "SSH key-only enforcement is disabled by configuration."
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if [[ "${SSH_PORT_IN_USE}" -ne 1 ]]; then
|
|
330
|
+
log "SSH port policy skipped because SSH_PORT_IN_USE is disabled."
|
|
331
|
+
return 0
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
ports="$(ssh_effective_ports | sort -u | tr '\n' ' ')"
|
|
335
|
+
if [[ -z "${ports}" ]]; then
|
|
336
|
+
warn "Unable to determine SSH listening ports from configuration."
|
|
337
|
+
return 0
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
if contains_word "22" "${ports}"; then
|
|
341
|
+
port_ok=0
|
|
342
|
+
if [[ "${AUTO_REMEDIATE}" -eq 1 && "${ENABLE_SSH_PORT_REMEDIATION}" -eq 1 ]] \
|
|
343
|
+
&& remediate_ssh_port "${SSH_EXPECTED_PORT}"; then
|
|
344
|
+
ports="$(ssh_effective_ports | sort -u | tr '\n' ' ')"
|
|
345
|
+
if contains_word "22" "${ports}"; then
|
|
346
|
+
issue "SSH still exposes port 22 after remediation attempt."
|
|
347
|
+
else
|
|
348
|
+
log "SSH no longer uses port 22."
|
|
349
|
+
fi
|
|
350
|
+
else
|
|
351
|
+
issue "SSH is configured on port 22. Migrate to a non-default port such as ${SSH_EXPECTED_PORT}, allow it in the firewall, then verify with: ssh -p ${SSH_EXPECTED_PORT} <user>@<host>"
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if ! contains_word "${SSH_EXPECTED_PORT}" "${ports}"; then
|
|
356
|
+
port_ok=0
|
|
357
|
+
issue "SSH is not configured on the expected port ${SSH_EXPECTED_PORT}. Current configured ports: ${ports}"
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
if [[ "${port_ok}" -eq 1 ]]; then
|
|
361
|
+
log "SSH uses the expected non-default port ${SSH_EXPECTED_PORT}."
|
|
362
|
+
fi
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
list_listening_ports() {
|
|
366
|
+
ss -lntuH 2>/dev/null | awk '
|
|
367
|
+
{
|
|
368
|
+
split($5, parts, ":")
|
|
369
|
+
port = parts[length(parts)]
|
|
370
|
+
if (port ~ /^[0-9]+$/) {
|
|
371
|
+
print tolower($1) " " port
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
' | sort -u
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
block_port() {
|
|
378
|
+
local proto="$1"
|
|
379
|
+
local port="$2"
|
|
380
|
+
local backend
|
|
381
|
+
backend="$(detect_firewall_backend)"
|
|
382
|
+
|
|
383
|
+
case "${backend}" in
|
|
384
|
+
ufw)
|
|
385
|
+
run_privileged ufw deny "${port}/${proto}" >/dev/null 2>&1
|
|
386
|
+
;;
|
|
387
|
+
firewalld)
|
|
388
|
+
run_privileged firewall-cmd --permanent --remove-port="${port}/${proto}" >/dev/null 2>&1 \
|
|
389
|
+
&& run_privileged firewall-cmd --reload >/dev/null 2>&1
|
|
390
|
+
;;
|
|
391
|
+
*)
|
|
392
|
+
return 1
|
|
393
|
+
;;
|
|
394
|
+
esac
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
check_ports() {
|
|
398
|
+
local proto
|
|
399
|
+
local port
|
|
400
|
+
local unexpected=0
|
|
401
|
+
local current
|
|
402
|
+
|
|
403
|
+
while read -r proto port; do
|
|
404
|
+
[[ -z "${proto}" || -z "${port}" ]] && continue
|
|
405
|
+
|
|
406
|
+
if [[ "${proto}" == udp* ]]; then
|
|
407
|
+
current="${EXPECTED_UDP_PORTS}"
|
|
408
|
+
proto="udp"
|
|
409
|
+
else
|
|
410
|
+
current="${EXPECTED_TCP_PORTS}"
|
|
411
|
+
proto="tcp"
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
if contains_word "${port}" "${current}"; then
|
|
415
|
+
continue
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
unexpected=1
|
|
419
|
+
if [[ "${AUTO_REMEDIATE}" -eq 1 && "${REMEDIATE_UNEXPECTED_PORTS}" -eq 1 ]] && block_port "${proto}" "${port}"; then
|
|
420
|
+
action "Blocked unexpected ${proto} port ${port} at the firewall."
|
|
421
|
+
else
|
|
422
|
+
issue "Unexpected listening ${proto} port detected: ${port}."
|
|
423
|
+
fi
|
|
424
|
+
done < <(list_listening_ports)
|
|
425
|
+
|
|
426
|
+
if [[ "${unexpected}" -eq 0 ]]; then
|
|
427
|
+
log "No unexpected listening ports detected."
|
|
428
|
+
fi
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
check_docker() {
|
|
432
|
+
local running
|
|
433
|
+
local name
|
|
434
|
+
local unexpected=0
|
|
435
|
+
|
|
436
|
+
if ! command_exists docker; then
|
|
437
|
+
log "Docker is not installed."
|
|
438
|
+
return 0
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
if ! docker info >/dev/null 2>&1; then
|
|
442
|
+
warn "Docker is installed but unavailable to the current user."
|
|
443
|
+
return 0
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
running="$(docker ps --format '{{.Names}}' 2>/dev/null)"
|
|
447
|
+
if [[ -z "${running}" ]]; then
|
|
448
|
+
log "No running Docker containers."
|
|
449
|
+
return 0
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
if [[ -z "${ALLOWED_DOCKER_CONTAINERS}" ]]; then
|
|
453
|
+
issue "Running Docker containers found but ALLOWED_DOCKER_CONTAINERS is not configured."
|
|
454
|
+
printf '%s\n' "${running}" | sed 's/^/[INFO] Running container: /'
|
|
455
|
+
return 0
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
while read -r name; do
|
|
459
|
+
[[ -z "${name}" ]] && continue
|
|
460
|
+
if contains_word "${name}" "${ALLOWED_DOCKER_CONTAINERS}"; then
|
|
461
|
+
continue
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
unexpected=1
|
|
465
|
+
if [[ "${AUTO_REMEDIATE}" -eq 1 ]] && docker stop "${name}" >/dev/null 2>&1; then
|
|
466
|
+
action "Stopped unexpected Docker container ${name}."
|
|
467
|
+
else
|
|
468
|
+
issue "Unexpected Docker container detected: ${name}."
|
|
469
|
+
fi
|
|
470
|
+
done <<< "${running}"
|
|
471
|
+
|
|
472
|
+
if [[ "${unexpected}" -eq 0 ]]; then
|
|
473
|
+
log "Docker containers match the allowlist."
|
|
474
|
+
fi
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
cleanup_disk_space() {
|
|
478
|
+
command_exists journalctl && run_privileged journalctl --vacuum-time=7d >/dev/null 2>&1 || true
|
|
479
|
+
command_exists apt-get && run_privileged apt-get clean >/dev/null 2>&1 || true
|
|
480
|
+
command_exists dnf && run_privileged dnf clean all >/dev/null 2>&1 || true
|
|
481
|
+
command_exists yum && run_privileged yum clean all >/dev/null 2>&1 || true
|
|
482
|
+
if [[ "${ALLOW_DOCKER_PRUNE}" -eq 1 ]] && command_exists docker; then
|
|
483
|
+
docker system prune -af >/dev/null 2>&1 || true
|
|
484
|
+
fi
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
enable_auto_security_updates_apt() {
|
|
488
|
+
local periodic_file="/etc/apt/apt.conf.d/20auto-upgrades"
|
|
489
|
+
local origin_file="/etc/apt/apt.conf.d/52openclaw-unattended-upgrades"
|
|
490
|
+
local distro_id version_codename tmp_file
|
|
491
|
+
|
|
492
|
+
distro_id="$(. /etc/os-release 2>/dev/null; printf '%s' "${ID:-Ubuntu}")"
|
|
493
|
+
version_codename="$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_CODENAME:-}")"
|
|
494
|
+
[[ -n "${version_codename}" ]] || version_codename="stable"
|
|
495
|
+
|
|
496
|
+
if ! command_exists apt-get; then
|
|
497
|
+
return 1
|
|
498
|
+
fi
|
|
499
|
+
|
|
500
|
+
run_privileged apt-get update >/dev/null 2>&1 || true
|
|
501
|
+
if ! dpkg -s unattended-upgrades >/dev/null 2>&1; then
|
|
502
|
+
run_privileged apt-get install -y unattended-upgrades >/dev/null 2>&1 || return 1
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
tmp_file="$(mktemp)"
|
|
506
|
+
cat >"${tmp_file}" <<'EOF'
|
|
507
|
+
APT::Periodic::Update-Package-Lists "1";
|
|
508
|
+
APT::Periodic::Unattended-Upgrade "1";
|
|
509
|
+
EOF
|
|
510
|
+
run_privileged install -m 0644 "${tmp_file}" "${periodic_file}" || { rm -f "${tmp_file}"; return 1; }
|
|
511
|
+
|
|
512
|
+
cat >"${tmp_file}" <<EOF
|
|
513
|
+
Unattended-Upgrade::Origins-Pattern {
|
|
514
|
+
"origin=${distro_id},codename=${version_codename},label=${distro_id}-Security";
|
|
515
|
+
"origin=${distro_id},codename=${version_codename},suite=${version_codename}-security";
|
|
516
|
+
"origin=Debian,codename=${version_codename},label=Debian-Security";
|
|
517
|
+
"origin=Debian,codename=${version_codename},suite=${version_codename}-security";
|
|
518
|
+
"origin=Ubuntu,codename=${version_codename},archive=${version_codename}-security";
|
|
519
|
+
};
|
|
520
|
+
EOF
|
|
521
|
+
run_privileged install -m 0644 "${tmp_file}" "${origin_file}" || { rm -f "${tmp_file}"; return 1; }
|
|
522
|
+
rm -f "${tmp_file}"
|
|
523
|
+
|
|
524
|
+
if command_exists systemctl; then
|
|
525
|
+
run_privileged systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
action "Enabled unattended-upgrades for security updates."
|
|
529
|
+
return 0
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
enable_auto_security_updates_dnf() {
|
|
533
|
+
local config_file="/etc/dnf/automatic.conf"
|
|
534
|
+
local timer_service=""
|
|
535
|
+
local tmp_file
|
|
536
|
+
|
|
537
|
+
if ! command_exists dnf; then
|
|
538
|
+
return 1
|
|
539
|
+
fi
|
|
540
|
+
|
|
541
|
+
run_privileged dnf -y install dnf-automatic >/dev/null 2>&1 || return 1
|
|
542
|
+
|
|
543
|
+
if command_exists systemctl; then
|
|
544
|
+
if systemctl list-unit-files 2>/dev/null | grep -q '^dnf-automatic-install.timer'; then
|
|
545
|
+
timer_service="dnf-automatic-install.timer"
|
|
546
|
+
elif systemctl list-unit-files 2>/dev/null | grep -q '^dnf-automatic.timer'; then
|
|
547
|
+
timer_service="dnf-automatic.timer"
|
|
548
|
+
fi
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
if [[ -f "${config_file}" ]]; then
|
|
552
|
+
tmp_file="$(mktemp)"
|
|
553
|
+
awk '
|
|
554
|
+
/^\s*upgrade_type\s*=/ { print "upgrade_type = security"; next }
|
|
555
|
+
/^\s*apply_updates\s*=/ { print "apply_updates = yes"; next }
|
|
556
|
+
{ print }
|
|
557
|
+
' "${config_file}" > "${tmp_file}"
|
|
558
|
+
run_privileged install -m 0644 "${tmp_file}" "${config_file}" || { rm -f "${tmp_file}"; return 1; }
|
|
559
|
+
rm -f "${tmp_file}"
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
[[ -n "${timer_service}" ]] || return 1
|
|
563
|
+
run_privileged systemctl enable --now "${timer_service}" >/dev/null 2>&1 || return 1
|
|
564
|
+
action "Enabled dnf-automatic security updates."
|
|
565
|
+
return 0
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
check_auto_security_updates() {
|
|
569
|
+
if [[ "${AUTO_SECURITY_UPDATES_REQUIRED}" -ne 1 ]]; then
|
|
570
|
+
log "Automatic security updates check is disabled by configuration."
|
|
571
|
+
return 0
|
|
572
|
+
fi
|
|
573
|
+
|
|
574
|
+
if command_exists apt-config || command_exists apt-get; then
|
|
575
|
+
if [[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] \
|
|
576
|
+
&& grep -Eq 'APT::Periodic::Unattended-Upgrade "1"' /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null \
|
|
577
|
+
&& dpkg -s unattended-upgrades >/dev/null 2>&1; then
|
|
578
|
+
log "Automatic security updates are enabled via unattended-upgrades."
|
|
579
|
+
return 0
|
|
580
|
+
fi
|
|
581
|
+
|
|
582
|
+
if [[ "${AUTO_REMEDIATE}" -eq 1 ]] && enable_auto_security_updates_apt; then
|
|
583
|
+
return 0
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
issue "Automatic security updates are not enabled on this APT host."
|
|
587
|
+
return 0
|
|
588
|
+
fi
|
|
589
|
+
|
|
590
|
+
if command_exists dnf; then
|
|
591
|
+
if command_exists systemctl \
|
|
592
|
+
&& (systemctl is-enabled --quiet dnf-automatic-install.timer 2>/dev/null || systemctl is-enabled --quiet dnf-automatic.timer 2>/dev/null) \
|
|
593
|
+
&& grep -Eq '^\s*upgrade_type\s*=\s*security' /etc/dnf/automatic.conf 2>/dev/null; then
|
|
594
|
+
log "Automatic security updates are enabled via dnf-automatic."
|
|
595
|
+
return 0
|
|
596
|
+
fi
|
|
597
|
+
|
|
598
|
+
if [[ "${AUTO_REMEDIATE}" -eq 1 ]] && enable_auto_security_updates_dnf; then
|
|
599
|
+
return 0
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
issue "Automatic security updates are not enabled on this DNF host."
|
|
603
|
+
return 0
|
|
604
|
+
fi
|
|
605
|
+
|
|
606
|
+
warn "Automatic security update verification is not implemented for this package manager."
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
check_disk() {
|
|
610
|
+
local line
|
|
611
|
+
local usage
|
|
612
|
+
local mountpoint
|
|
613
|
+
local over_limit=0
|
|
614
|
+
|
|
615
|
+
while read -r line; do
|
|
616
|
+
usage="$(awk '{print $5}' <<< "${line}" | tr -d '%')"
|
|
617
|
+
mountpoint="$(awk '{print $6}' <<< "${line}")"
|
|
618
|
+
[[ "${usage}" =~ ^[0-9]+$ ]] || continue
|
|
619
|
+
|
|
620
|
+
if (( usage < DISK_THRESHOLD_PERCENT )); then
|
|
621
|
+
continue
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
over_limit=1
|
|
625
|
+
if [[ "${AUTO_REMEDIATE}" -eq 1 ]]; then
|
|
626
|
+
cleanup_disk_space
|
|
627
|
+
usage="$(df -P "${mountpoint}" | awk 'NR==2 {gsub("%","",$5); print $5}')"
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
if [[ "${usage}" =~ ^[0-9]+$ ]] && (( usage < DISK_THRESHOLD_PERCENT )); then
|
|
631
|
+
action "Reduced disk usage on ${mountpoint} to ${usage}%."
|
|
632
|
+
else
|
|
633
|
+
issue "Disk usage on ${mountpoint} is ${usage}% (threshold ${DISK_THRESHOLD_PERCENT}%)."
|
|
634
|
+
fi
|
|
635
|
+
done < <(df -P -x tmpfs -x devtmpfs 2>/dev/null | awk 'NR>1')
|
|
636
|
+
|
|
637
|
+
if [[ "${over_limit}" -eq 0 ]]; then
|
|
638
|
+
log "Disk usage is below ${DISK_THRESHOLD_PERCENT}% on persistent filesystems."
|
|
639
|
+
fi
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
failed_login_count() {
|
|
643
|
+
local count=0
|
|
644
|
+
|
|
645
|
+
if command_exists journalctl; then
|
|
646
|
+
count="$(journalctl --since '-24 hours' --no-pager 2>/dev/null | grep -Eci 'Failed password|authentication failure|Invalid user|Failed publickey')"
|
|
647
|
+
echo "${count}"
|
|
648
|
+
return 0
|
|
649
|
+
fi
|
|
650
|
+
|
|
651
|
+
if command_exists lastb; then
|
|
652
|
+
count="$(lastb -s -24hours 2>/dev/null | awk 'NF > 0 && $1 != "btmp" { total++ } END { print total + 0 }')"
|
|
653
|
+
echo "${count}"
|
|
654
|
+
return 0
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
if [[ -f /var/log/auth.log ]]; then
|
|
658
|
+
count="$(find /var/log -maxdepth 1 -name 'auth.log*' -mtime -1 -print0 2>/dev/null | xargs -0 zgrep -Ehi 'Failed password|authentication failure|Invalid user|Failed publickey' 2>/dev/null | wc -l | tr -d ' ')"
|
|
659
|
+
echo "${count:-0}"
|
|
660
|
+
return 0
|
|
661
|
+
fi
|
|
662
|
+
|
|
663
|
+
echo "0"
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
check_failed_logins() {
|
|
667
|
+
local count
|
|
668
|
+
count="$(failed_login_count)"
|
|
669
|
+
log "Failed login attempts in the last 24 hours: ${count}."
|
|
670
|
+
|
|
671
|
+
if [[ "${count}" =~ ^[0-9]+$ ]] && (( count > MAX_FAILED_LOGIN_ATTEMPTS )); then
|
|
672
|
+
issue "Failed login attempts exceed the threshold (${count} > ${MAX_FAILED_LOGIN_ATTEMPTS})."
|
|
673
|
+
fi
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
check_virustotal() {
|
|
677
|
+
local helper
|
|
678
|
+
local item
|
|
679
|
+
local any_item=0
|
|
680
|
+
|
|
681
|
+
if [[ "${VIRUSTOTAL_ENABLED}" -ne 1 ]]; then
|
|
682
|
+
log "VirusTotal checks are disabled."
|
|
683
|
+
return 0
|
|
684
|
+
fi
|
|
685
|
+
|
|
686
|
+
helper="${SCRIPT_DIR}/openclaw_virustotal_check.sh"
|
|
687
|
+
if [[ ! -x "${helper}" ]]; then
|
|
688
|
+
issue "VirusTotal helper script is missing or not executable: ${helper}"
|
|
689
|
+
return 0
|
|
690
|
+
fi
|
|
691
|
+
|
|
692
|
+
for item in ${VIRUSTOTAL_URLS}; do
|
|
693
|
+
any_item=1
|
|
694
|
+
if VIRUSTOTAL_ALLOW_UPLOADS="${VIRUSTOTAL_ALLOW_UPLOADS}" \
|
|
695
|
+
"${helper}" --url "${item}"; then
|
|
696
|
+
log "VirusTotal browser review prepared for URL: ${item}"
|
|
697
|
+
else
|
|
698
|
+
issue "VirusTotal browser workflow failed for URL: ${item}"
|
|
699
|
+
fi
|
|
700
|
+
done
|
|
701
|
+
|
|
702
|
+
for item in ${VIRUSTOTAL_FILES}; do
|
|
703
|
+
any_item=1
|
|
704
|
+
if VIRUSTOTAL_ALLOW_UPLOADS="${VIRUSTOTAL_ALLOW_UPLOADS}" \
|
|
705
|
+
"${helper}" --file "${item}"; then
|
|
706
|
+
log "VirusTotal browser review prepared for file: ${item}"
|
|
707
|
+
else
|
|
708
|
+
issue "VirusTotal browser workflow failed for file: ${item}"
|
|
709
|
+
fi
|
|
710
|
+
done
|
|
711
|
+
|
|
712
|
+
if [[ "${any_item}" -eq 0 ]]; then
|
|
713
|
+
warn "VirusTotal is enabled but no VIRUSTOTAL_URLS or VIRUSTOTAL_FILES were configured."
|
|
714
|
+
fi
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
main() {
|
|
718
|
+
log "Starting OpenClaw security audit."
|
|
719
|
+
[[ -n "${CONFIG_FILE}" ]] && log "Using configuration file: ${CONFIG_FILE}"
|
|
720
|
+
|
|
721
|
+
check_firewall
|
|
722
|
+
check_fail2ban
|
|
723
|
+
check_ssh
|
|
724
|
+
check_ports
|
|
725
|
+
check_docker
|
|
726
|
+
check_disk
|
|
727
|
+
check_failed_logins
|
|
728
|
+
check_auto_security_updates
|
|
729
|
+
check_virustotal
|
|
730
|
+
|
|
731
|
+
if (( ${#ISSUES[@]} == 0 )); then
|
|
732
|
+
printf 'audit de sécurité réussi\n'
|
|
733
|
+
exit 0
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
printf '\nSecurity audit found %d issue(s).\n' "${#ISSUES[@]}"
|
|
737
|
+
printf '%s\n' "${ISSUES[@]}" | sed 's/^/- /'
|
|
738
|
+
|
|
739
|
+
if (( ${#ACTIONS[@]} > 0 )); then
|
|
740
|
+
printf '\nRemediation actions applied:\n'
|
|
741
|
+
printf '%s\n' "${ACTIONS[@]}" | sed 's/^/- /'
|
|
742
|
+
fi
|
|
743
|
+
|
|
744
|
+
if (( ${#WARNINGS[@]} > 0 )); then
|
|
745
|
+
printf '\nWarnings:\n'
|
|
746
|
+
printf '%s\n' "${WARNINGS[@]}" | sed 's/^/- /'
|
|
747
|
+
fi
|
|
748
|
+
|
|
749
|
+
exit 1
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
main "$@"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
VIRUSTOTAL_ALLOW_UPLOADS="${VIRUSTOTAL_ALLOW_UPLOADS:-0}"
|
|
6
|
+
|
|
7
|
+
usage() {
|
|
8
|
+
cat <<'EOF'
|
|
9
|
+
Usage:
|
|
10
|
+
openclaw_virustotal_check.sh --url <url>
|
|
11
|
+
openclaw_virustotal_check.sh --file <path>
|
|
12
|
+
|
|
13
|
+
This helper is API-free. It prepares public VirusTotal review steps for the
|
|
14
|
+
OpenClaw browser tool.
|
|
15
|
+
EOF
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fail() {
|
|
19
|
+
printf '[FAIL] %s\n' "$*" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
info() {
|
|
24
|
+
printf '[INFO] %s\n' "$*"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
warn() {
|
|
28
|
+
printf '[WARN] %s\n' "$*"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
require_file() {
|
|
32
|
+
[[ -f "$1" ]] || fail "File not found: $1"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
sha256_file() {
|
|
36
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
37
|
+
sha256sum "$1" | awk '{print $1}'
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
41
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
fail "No SHA-256 tool found. Install sha256sum or shasum."
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
check_url() {
|
|
48
|
+
local url="$1"
|
|
49
|
+
|
|
50
|
+
[[ -n "${url}" ]] || fail "URL must not be empty."
|
|
51
|
+
|
|
52
|
+
info "VirusTotal API-free URL workflow prepared."
|
|
53
|
+
printf 'VT_BROWSER_URL=%s\n' "https://www.virustotal.com/gui/home/url"
|
|
54
|
+
printf 'VT_SUBMIT_URL=%s\n' "${url}"
|
|
55
|
+
printf 'OPENCLAW_BROWSER_COMMAND=%s\n' "browser.navigate https://www.virustotal.com/gui/home/url"
|
|
56
|
+
printf 'OPENCLAW_BROWSER_NOTE=%s\n' "Use browser.snapshot and browser.act to submit the URL and inspect detections."
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
check_file() {
|
|
60
|
+
local file_path="$1"
|
|
61
|
+
local sha256
|
|
62
|
+
local report_url
|
|
63
|
+
|
|
64
|
+
require_file "${file_path}"
|
|
65
|
+
sha256="$(sha256_file "${file_path}")"
|
|
66
|
+
report_url="https://www.virustotal.com/gui/file/${sha256}/detection"
|
|
67
|
+
|
|
68
|
+
info "VirusTotal API-free file workflow prepared."
|
|
69
|
+
printf 'VT_FILE_SHA256=%s\n' "${sha256}"
|
|
70
|
+
printf 'VT_GUI_REPORT=%s\n' "${report_url}"
|
|
71
|
+
printf 'VT_BROWSER_UPLOAD=%s\n' "https://www.virustotal.com/gui/home/upload"
|
|
72
|
+
printf 'OPENCLAW_BROWSER_NOTE=%s\n' "Open the report URL first. If no report exists and uploads are approved, use browser.upload on the upload page."
|
|
73
|
+
|
|
74
|
+
if [[ "${VIRUSTOTAL_ALLOW_UPLOADS}" -eq 1 ]]; then
|
|
75
|
+
printf 'VT_UPLOAD_ALLOWED=%s\n' "1"
|
|
76
|
+
else
|
|
77
|
+
printf 'VT_UPLOAD_ALLOWED=%s\n' "0"
|
|
78
|
+
warn "Website upload is disabled unless the user explicitly approves it."
|
|
79
|
+
fi
|
|
80
|
+
printf 'USER_DECISION_REQUIRED=%s\n' "If VirusTotal flags this file, ask the user whether to keep or remove it."
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
main() {
|
|
84
|
+
case "${1:-}" in
|
|
85
|
+
--url)
|
|
86
|
+
[[ $# -eq 2 ]] || { usage; exit 1; }
|
|
87
|
+
check_url "$2"
|
|
88
|
+
;;
|
|
89
|
+
--file)
|
|
90
|
+
[[ $# -eq 2 ]] || { usage; exit 1; }
|
|
91
|
+
check_file "$2"
|
|
92
|
+
;;
|
|
93
|
+
*)
|
|
94
|
+
usage
|
|
95
|
+
exit 1
|
|
96
|
+
;;
|
|
97
|
+
esac
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main "$@"
|