safecrab 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 +220 -0
- package/dist/cli/commands/scan.d.ts +5 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +47 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +23 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/engine/context.d.ts +6 -0
- package/dist/engine/context.d.ts.map +1 -0
- package/dist/engine/context.js +33 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/exposure.d.ts +22 -0
- package/dist/engine/exposure.d.ts.map +1 -0
- package/dist/engine/exposure.js +100 -0
- package/dist/engine/exposure.js.map +1 -0
- package/dist/engine/heuristics.d.ts +11 -0
- package/dist/engine/heuristics.d.ts.map +1 -0
- package/dist/engine/heuristics.js +219 -0
- package/dist/engine/heuristics.js.map +1 -0
- package/dist/engine/types.d.ts +46 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +5 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/system/collectors/cloudflare.d.ts +8 -0
- package/dist/system/collectors/cloudflare.d.ts.map +1 -0
- package/dist/system/collectors/cloudflare.js +32 -0
- package/dist/system/collectors/cloudflare.js.map +1 -0
- package/dist/system/collectors/firewall.d.ts +10 -0
- package/dist/system/collectors/firewall.d.ts.map +1 -0
- package/dist/system/collectors/firewall.js +69 -0
- package/dist/system/collectors/firewall.js.map +1 -0
- package/dist/system/collectors/network.d.ts +10 -0
- package/dist/system/collectors/network.d.ts.map +1 -0
- package/dist/system/collectors/network.js +18 -0
- package/dist/system/collectors/network.js.map +1 -0
- package/dist/system/collectors/services.d.ts +6 -0
- package/dist/system/collectors/services.d.ts.map +1 -0
- package/dist/system/collectors/services.js +31 -0
- package/dist/system/collectors/services.js.map +1 -0
- package/dist/system/collectors/tailscale.d.ts +10 -0
- package/dist/system/collectors/tailscale.d.ts.map +1 -0
- package/dist/system/collectors/tailscale.js +39 -0
- package/dist/system/collectors/tailscale.js.map +1 -0
- package/dist/system/parsers/ip-parser.d.ts +25 -0
- package/dist/system/parsers/ip-parser.d.ts.map +1 -0
- package/dist/system/parsers/ip-parser.js +100 -0
- package/dist/system/parsers/ip-parser.js.map +1 -0
- package/dist/system/parsers/ss-parser.d.ts +15 -0
- package/dist/system/parsers/ss-parser.d.ts.map +1 -0
- package/dist/system/parsers/ss-parser.js +153 -0
- package/dist/system/parsers/ss-parser.js.map +1 -0
- package/dist/system/shell.d.ts +27 -0
- package/dist/system/shell.d.ts.map +1 -0
- package/dist/system/shell.js +56 -0
- package/dist/system/shell.js.map +1 -0
- package/dist/ui/findings.d.ts +6 -0
- package/dist/ui/findings.d.ts.map +1 -0
- package/dist/ui/findings.js +91 -0
- package/dist/ui/findings.js.map +1 -0
- package/dist/ui/renderer.d.ts +18 -0
- package/dist/ui/renderer.d.ts.map +1 -0
- package/dist/ui/renderer.js +33 -0
- package/dist/ui/renderer.js.map +1 -0
- package/dist/ui/summary.d.ts +17 -0
- package/dist/ui/summary.d.ts.map +1 -0
- package/dist/ui/summary.js +39 -0
- package/dist/ui/summary.js.map +1 -0
- package/dist/ui/theme.d.ts +39 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +47 -0
- package/dist/ui/theme.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Safecrab Contributors
|
|
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,220 @@
|
|
|
1
|
+
# 🦀 Safecrab
|
|
2
|
+
|
|
3
|
+
**Security scanner for Linux VPS environments** — Detect accidental service exposure and false security assumptions.
|
|
4
|
+
|
|
5
|
+
> "It's the eslint of server exposure."
|
|
6
|
+
|
|
7
|
+
Safecrab is a **read-only** CLI tool that reveals the truth about what services are actually reachable on your Linux VPS, even when you think they're protected by tunnels, VPNs, or firewalls.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
Safecrab answers the critical question:
|
|
12
|
+
|
|
13
|
+
> **"What services are reachable, from where, and why?"**
|
|
14
|
+
|
|
15
|
+
Not just "Is the firewall enabled?"
|
|
16
|
+
|
|
17
|
+
### Key Features
|
|
18
|
+
|
|
19
|
+
- 🔍 **Detects listening services** (via `ss -tulnp`)
|
|
20
|
+
- 🌐 **Identifies exposure paths**: public internet, Tailscale, Cloudflare Tunnel, localhost
|
|
21
|
+
- 🚨 **Finds tunnel bypass scenarios**: When services are exposed publicly *despite* having tunnels
|
|
22
|
+
- 🤖 **Escalates AI/ML services**: Automatically treats exposed AI servers as critical risks
|
|
23
|
+
- 📊 **Beautiful terminal UI**: Clear, calm explanations without jargon
|
|
24
|
+
- 🔒 **100% read-only**: Makes zero system changes
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g safecrab
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or use directly with `npx`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx safecrab scan
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
safecrab scan
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For best visibility, run with root privileges:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
sudo safecrab scan
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> **Note**: Running without root may hide some services. Safecrab will warn you but continue with best-effort scanning.
|
|
51
|
+
|
|
52
|
+
## Example Output
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
🦀 Safecrab Security Scan
|
|
56
|
+
|
|
57
|
+
Summary:
|
|
58
|
+
→ 5 services detected
|
|
59
|
+
→ 2 publicly reachable
|
|
60
|
+
→ 1 critical issues
|
|
61
|
+
|
|
62
|
+
CRITICAL
|
|
63
|
+
✖ Tunnel bypass detected
|
|
64
|
+
Port 3000 (ollama) is accessible via both Cloudflare Tunnel and directly
|
|
65
|
+
from the public internet. The tunnel does not protect this service.
|
|
66
|
+
|
|
67
|
+
Recommendation:
|
|
68
|
+
Bind the service to localhost (127.0.0.1) or restrict access via firewall
|
|
69
|
+
to ensure traffic only flows through the tunnel.
|
|
70
|
+
|
|
71
|
+
WARNINGS
|
|
72
|
+
⚠ Service exposed to public internet
|
|
73
|
+
Port 22 (sshd) is accessible from the public internet.
|
|
74
|
+
|
|
75
|
+
Recommendation:
|
|
76
|
+
Verify this service should be publicly accessible. If not, bind to
|
|
77
|
+
localhost or use firewall rules.
|
|
78
|
+
|
|
79
|
+
INFO
|
|
80
|
+
✔ Firewall is enabled
|
|
81
|
+
UFW firewall is active with default inbound policy: deny.
|
|
82
|
+
|
|
83
|
+
✔ Tailscale is connected
|
|
84
|
+
Tailscale VPN is active and available for secure access.
|
|
85
|
+
|
|
86
|
+
No changes were made to your system.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Who Is This For?
|
|
90
|
+
|
|
91
|
+
Safecrab is designed for:
|
|
92
|
+
|
|
93
|
+
- **Developers** running AI models, APIs, or dev servers on VPS instances
|
|
94
|
+
- **Self-hosters** managing services like Ollama, Jupyter, or internal dashboards
|
|
95
|
+
- **Security-conscious users** who want visibility without complexity
|
|
96
|
+
- **Anyone** who has ever said: *"Oh wow, I thought this was private."*
|
|
97
|
+
|
|
98
|
+
## Common Scenarios Detected
|
|
99
|
+
|
|
100
|
+
### 1. Tunnel Bypass (Critical)
|
|
101
|
+
|
|
102
|
+
You set up a Cloudflare Tunnel but forgot to bind your service to localhost. **Safecrab detects both paths** and warns you.
|
|
103
|
+
|
|
104
|
+
### 2. Exposed AI Services (Critical)
|
|
105
|
+
|
|
106
|
+
Ollama, LLaMA, Python servers, Node.js apps exposed to the public internet are automatically escalated to critical severity.
|
|
107
|
+
|
|
108
|
+
### 3. Tailscale Available But Unused (Warning)
|
|
109
|
+
|
|
110
|
+
You have Tailscale connected but services are still publicly accessible instead of using the VPN.
|
|
111
|
+
|
|
112
|
+
### 4. SSH Without Firewall (Warning)
|
|
113
|
+
|
|
114
|
+
SSH port 22 is reachable from the public internet without firewall protection.
|
|
115
|
+
|
|
116
|
+
## What Safecrab Does NOT Do
|
|
117
|
+
|
|
118
|
+
- ❌ Modify system configuration
|
|
119
|
+
- ❌ Change firewall rules
|
|
120
|
+
- ❌ Restart services
|
|
121
|
+
- ❌ Require credentials
|
|
122
|
+
- ❌ Make network requests
|
|
123
|
+
- ❌ Write any files
|
|
124
|
+
|
|
125
|
+
**Safecrab is 100% read-only.** It only observes and reports.
|
|
126
|
+
|
|
127
|
+
## How It Works
|
|
128
|
+
|
|
129
|
+
1. **Collects system facts**: Network interfaces, listening services, firewall status
|
|
130
|
+
2. **Detects security context**: Tailscale, Cloudflare Tunnel, UFW status
|
|
131
|
+
3. **Resolves exposure paths**: Determines which services are reachable from where
|
|
132
|
+
4. **Applies risk heuristics**: Categorizes findings by severity
|
|
133
|
+
5. **Renders human-readable report**: Beautiful terminal output with clear explanations
|
|
134
|
+
|
|
135
|
+
## Exit Codes
|
|
136
|
+
|
|
137
|
+
- `0` — No critical issues found
|
|
138
|
+
- `1` — Critical security issues detected
|
|
139
|
+
|
|
140
|
+
Use in scripts:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
safecrab scan
|
|
144
|
+
if [ $? -eq 1 ]; then
|
|
145
|
+
echo "Critical security issues found!"
|
|
146
|
+
exit 1
|
|
147
|
+
fi
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## System Requirements
|
|
151
|
+
|
|
152
|
+
- **OS**: Linux (Ubuntu, Debian, or similar)
|
|
153
|
+
- **Runtime**: Node.js 20 or later
|
|
154
|
+
- **Commands**: `ss`, `ip` (standard on most Linux systems)
|
|
155
|
+
- **Optional**: `ufw`, `tailscale`, `cloudflared` for enhanced detection
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Clone the repository
|
|
161
|
+
git clone https://github.com/isacssw/safecrab.git
|
|
162
|
+
cd safecrab
|
|
163
|
+
|
|
164
|
+
# Install dependencies
|
|
165
|
+
npm install
|
|
166
|
+
|
|
167
|
+
# Build
|
|
168
|
+
npm run build
|
|
169
|
+
|
|
170
|
+
# Run tests
|
|
171
|
+
npm test
|
|
172
|
+
|
|
173
|
+
# Run locally
|
|
174
|
+
npm run build && node dist/cli/index.js scan
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Architecture
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
safecrab/
|
|
181
|
+
├── src/
|
|
182
|
+
│ ├── system/ # Layer 1: Raw system facts
|
|
183
|
+
│ ├── engine/ # Layer 2: Interpreted truth
|
|
184
|
+
│ └── ui/ # Layer 3: Human presentation
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
See the [project specification](SPEC.md) for detailed architecture.
|
|
188
|
+
|
|
189
|
+
## Philosophy
|
|
190
|
+
|
|
191
|
+
> **"Safecrab does not enforce security. It reveals truth."**
|
|
192
|
+
|
|
193
|
+
Safecrab helps you understand your actual security posture, not your assumed security posture. Enforcement comes later — first, you need visibility.
|
|
194
|
+
|
|
195
|
+
## Roadmap
|
|
196
|
+
|
|
197
|
+
**MVP (Current)**: Read-only scanning with beautiful terminal output
|
|
198
|
+
|
|
199
|
+
**Post-MVP**:
|
|
200
|
+
- JSON output for CI/CD integration
|
|
201
|
+
- Automated fix suggestions
|
|
202
|
+
- Config file support
|
|
203
|
+
- GitHub Actions integration
|
|
204
|
+
- Single binary distribution
|
|
205
|
+
|
|
206
|
+
## Contributing
|
|
207
|
+
|
|
208
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
|
213
|
+
|
|
214
|
+
## Credits
|
|
215
|
+
|
|
216
|
+
Built with love for the self-hosting and security-conscious community.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
**If you found a service you didn't know was exposed, Safecrab succeeded.** ✨
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CA2CjD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan command - orchestrates the security scan
|
|
3
|
+
*/
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { buildNetworkContext } from "../../engine/context.js";
|
|
6
|
+
import { resolveAllExposures } from "../../engine/exposure.js";
|
|
7
|
+
import { analyzeExposures } from "../../engine/heuristics.js";
|
|
8
|
+
import { collectListeningServices } from "../../system/collectors/services.js";
|
|
9
|
+
import { getExitCode, renderReport } from "../../ui/renderer.js";
|
|
10
|
+
export async function scanCommand() {
|
|
11
|
+
let spinner = ora("Detecting services...").start();
|
|
12
|
+
try {
|
|
13
|
+
// Step 1: Collect listening services
|
|
14
|
+
const services = await collectListeningServices();
|
|
15
|
+
spinner.succeed(`Found ${services.length} listening services`);
|
|
16
|
+
// Step 2: Build network context
|
|
17
|
+
spinner = ora("Analyzing network context...").start();
|
|
18
|
+
const context = await buildNetworkContext();
|
|
19
|
+
spinner.succeed("Network context analyzed");
|
|
20
|
+
// Step 3: Resolve exposure paths
|
|
21
|
+
spinner = ora("Evaluating exposure paths...").start();
|
|
22
|
+
const exposures = resolveAllExposures(services, context);
|
|
23
|
+
spinner.succeed("Exposure analysis complete");
|
|
24
|
+
// Step 4: Run risk heuristics
|
|
25
|
+
spinner = ora("Running security checks...").start();
|
|
26
|
+
const findings = analyzeExposures(exposures, context);
|
|
27
|
+
spinner.succeed("Security analysis complete");
|
|
28
|
+
spinner.stop();
|
|
29
|
+
// Step 5: Render report
|
|
30
|
+
const report = renderReport({ services, findings });
|
|
31
|
+
console.log(`\n${report}\n`);
|
|
32
|
+
// Step 6: Exit with appropriate code
|
|
33
|
+
const exitCode = getExitCode(findings);
|
|
34
|
+
process.exit(exitCode);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
spinner.fail("Scan failed");
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
console.error(`\nError: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.error("\nAn unknown error occurred");
|
|
43
|
+
}
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjE,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,OAAO,GAAG,GAAG,CAAC,uBAAuB,CAAC,CAAC,KAAK,EAAE,CAAC;IAEnD,IAAI,CAAC;QACH,qCAAqC;QACrC,MAAM,QAAQ,GAAG,MAAM,wBAAwB,EAAE,CAAC;QAClD,OAAO,CAAC,OAAO,CAAC,SAAS,QAAQ,CAAC,MAAM,qBAAqB,CAAC,CAAC;QAE/D,gCAAgC;QAChC,OAAO,GAAG,GAAG,CAAC,8BAA8B,CAAC,CAAC,KAAK,EAAE,CAAC;QACtD,MAAM,OAAO,GAAG,MAAM,mBAAmB,EAAE,CAAC;QAC5C,OAAO,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC;QAE5C,iCAAiC;QACjC,OAAO,GAAG,GAAG,CAAC,8BAA8B,CAAC,CAAC,KAAK,EAAE,CAAC;QACtD,MAAM,SAAS,GAAG,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACzD,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAC,CAAC;QAE9C,8BAA8B;QAC9B,OAAO,GAAG,GAAG,CAAC,4BAA4B,CAAC,CAAC,KAAK,EAAE,CAAC;QACpD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACtD,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAC,CAAC;QAE9C,OAAO,CAAC,IAAI,EAAE,CAAC;QAEf,wBAAwB;QACxB,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QACpD,OAAO,CAAC,GAAG,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC;QAE7B,qCAAqC;QACrC,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACvC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAE5B,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,YAAY,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAC/C,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA;;GAEG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Safecrab CLI entry point
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { scanCommand } from "./commands/scan.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("safecrab")
|
|
10
|
+
.description("Security scanner for Linux VPS environments - detect accidental service exposure")
|
|
11
|
+
.version("0.1.0");
|
|
12
|
+
program
|
|
13
|
+
.command("scan")
|
|
14
|
+
.description("Scan for exposed services and security issues")
|
|
15
|
+
.action(async () => {
|
|
16
|
+
await scanCommand();
|
|
17
|
+
});
|
|
18
|
+
// If no command specified, default to scan
|
|
19
|
+
if (process.argv.length === 2) {
|
|
20
|
+
process.argv.push("scan");
|
|
21
|
+
}
|
|
22
|
+
program.parse();
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,kFAAkF,CAAC;KAC/F,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,+CAA+C,CAAC;KAC5D,MAAM,CAAC,KAAK,IAAI,EAAE;IACjB,MAAM,WAAW,EAAE,CAAC;AACtB,CAAC,CAAC,CAAC;AAEL,2CAA2C;AAC3C,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC9B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/engine/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,cAAc,CAAC,CAyBnE"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build NetworkContext from system collectors
|
|
3
|
+
*/
|
|
4
|
+
import { collectCloudflareStatus } from "../system/collectors/cloudflare.js";
|
|
5
|
+
import { collectFirewallStatus } from "../system/collectors/firewall.js";
|
|
6
|
+
import { collectNetworkInterfaces } from "../system/collectors/network.js";
|
|
7
|
+
import { collectTailscaleStatus } from "../system/collectors/tailscale.js";
|
|
8
|
+
export async function buildNetworkContext() {
|
|
9
|
+
// Run all collectors in parallel
|
|
10
|
+
const [interfaces, firewall, tailscale, cloudflare] = await Promise.all([
|
|
11
|
+
collectNetworkInterfaces(),
|
|
12
|
+
collectFirewallStatus(),
|
|
13
|
+
collectTailscaleStatus(),
|
|
14
|
+
collectCloudflareStatus(),
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
firewall: {
|
|
18
|
+
enabled: firewall.enabled,
|
|
19
|
+
defaultInbound: firewall.defaultInbound,
|
|
20
|
+
statusKnown: firewall.statusKnown,
|
|
21
|
+
},
|
|
22
|
+
tailscale: {
|
|
23
|
+
installed: tailscale.installed,
|
|
24
|
+
connected: tailscale.connected,
|
|
25
|
+
interface: tailscale.interface,
|
|
26
|
+
},
|
|
27
|
+
cloudflare: {
|
|
28
|
+
tunnelDetected: cloudflare.tunnelDetected,
|
|
29
|
+
},
|
|
30
|
+
interfaces,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/engine/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAG3E,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,iCAAiC;IACjC,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACtE,wBAAwB,EAAE;QAC1B,qBAAqB,EAAE;QACvB,sBAAsB,EAAE;QACxB,uBAAuB,EAAE;KAC1B,CAAC,CAAC;IAEH,OAAO;QACL,QAAQ,EAAE;YACR,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,cAAc,EAAE,QAAQ,CAAC,cAAc;YACvC,WAAW,EAAE,QAAQ,CAAC,WAAW;SAClC;QACD,SAAS,EAAE;YACT,SAAS,EAAE,SAAS,CAAC,SAAS;YAC9B,SAAS,EAAE,SAAS,CAAC,SAAS;YAC9B,SAAS,EAAE,SAAS,CAAC,SAAS;SAC/B;QACD,UAAU,EAAE;YACV,cAAc,EAAE,UAAU,CAAC,cAAc;SAC1C;QACD,UAAU;KACX,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exposure resolution logic
|
|
3
|
+
* Determines which exposure paths each service has
|
|
4
|
+
*/
|
|
5
|
+
import type { ExposurePath, ListeningService, NetworkContext, ServiceExposure } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve exposure paths for a listening service
|
|
8
|
+
*
|
|
9
|
+
* Logic:
|
|
10
|
+
* - 127.0.0.1 binding → localhost-only
|
|
11
|
+
* - Public interface + firewall allows → public-internet
|
|
12
|
+
* - tailscale0 interface → tailscale
|
|
13
|
+
* - Cloudflare tunnel detected → cloudflare-tunnel
|
|
14
|
+
*
|
|
15
|
+
* Multiple paths can coexist (e.g., tunnel + public = bypass)
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveExposure(service: ListeningService, context: NetworkContext): ExposurePath[];
|
|
18
|
+
/**
|
|
19
|
+
* Resolve exposure for all services
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveAllExposures(services: ListeningService[], context: NetworkContext): ServiceExposure[];
|
|
22
|
+
//# sourceMappingURL=exposure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exposure.d.ts","sourceRoot":"","sources":["../../src/engine/exposure.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElG;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,cAAc,GACtB,YAAY,EAAE,CA6BhB;AAkED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,OAAO,EAAE,cAAc,GACtB,eAAe,EAAE,CAKnB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exposure resolution logic
|
|
3
|
+
* Determines which exposure paths each service has
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Resolve exposure paths for a listening service
|
|
7
|
+
*
|
|
8
|
+
* Logic:
|
|
9
|
+
* - 127.0.0.1 binding → localhost-only
|
|
10
|
+
* - Public interface + firewall allows → public-internet
|
|
11
|
+
* - tailscale0 interface → tailscale
|
|
12
|
+
* - Cloudflare tunnel detected → cloudflare-tunnel
|
|
13
|
+
*
|
|
14
|
+
* Multiple paths can coexist (e.g., tunnel + public = bypass)
|
|
15
|
+
*/
|
|
16
|
+
export function resolveExposure(service, context) {
|
|
17
|
+
const paths = [];
|
|
18
|
+
// Check if localhost only
|
|
19
|
+
if (isLocalhostOnly(service)) {
|
|
20
|
+
return ["localhost-only"];
|
|
21
|
+
}
|
|
22
|
+
// Check for Tailscale exposure
|
|
23
|
+
if (isTailscaleExposed(service, context)) {
|
|
24
|
+
paths.push("tailscale");
|
|
25
|
+
}
|
|
26
|
+
// Check for Cloudflare tunnel
|
|
27
|
+
if (context.cloudflare.tunnelDetected) {
|
|
28
|
+
paths.push("cloudflare-tunnel");
|
|
29
|
+
}
|
|
30
|
+
// Check for public internet exposure
|
|
31
|
+
if (isPublicExposed(service, context)) {
|
|
32
|
+
paths.push("public-internet");
|
|
33
|
+
}
|
|
34
|
+
// If no paths determined, assume localhost
|
|
35
|
+
if (paths.length === 0) {
|
|
36
|
+
return ["localhost-only"];
|
|
37
|
+
}
|
|
38
|
+
return paths;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if service is localhost-only
|
|
42
|
+
*/
|
|
43
|
+
function isLocalhostOnly(service) {
|
|
44
|
+
// Bound to localhost IP
|
|
45
|
+
if (service.boundIp === "127.0.0.1" ||
|
|
46
|
+
service.boundIp === "::1" ||
|
|
47
|
+
service.boundIp === "localhost") {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// Only bound to loopback interface
|
|
51
|
+
if (service.interfaces.length === 1 && service.interfaces[0] === "lo") {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if service is exposed via Tailscale
|
|
58
|
+
*/
|
|
59
|
+
function isTailscaleExposed(service, context) {
|
|
60
|
+
if (!context.tailscale.connected) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const tailscaleInterface = context.tailscale.interface ?? "tailscale0";
|
|
64
|
+
// Service is bound to tailscale interface
|
|
65
|
+
return service.interfaces.includes(tailscaleInterface);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if service is exposed to public internet
|
|
69
|
+
*/
|
|
70
|
+
function isPublicExposed(service, context) {
|
|
71
|
+
// Check if bound to any public interface
|
|
72
|
+
const hasPublicInterface = service.interfaces.some((iface) => {
|
|
73
|
+
const interfaceInfo = context.interfaces.find((i) => i.name === iface);
|
|
74
|
+
return interfaceInfo?.isPublic ?? false;
|
|
75
|
+
});
|
|
76
|
+
if (!hasPublicInterface) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// If firewall is enabled with deny default, service might be blocked
|
|
80
|
+
// However, we assume public exposure unless explicitly proven otherwise
|
|
81
|
+
// (conservative security assumption)
|
|
82
|
+
// If firewall explicitly allows all, definitely exposed
|
|
83
|
+
if (context.firewall.defaultInbound === "allow") {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
// If firewall denies by default, still assume exposed
|
|
87
|
+
// (we can't determine specific rules without deep inspection)
|
|
88
|
+
// This is intentionally conservative
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve exposure for all services
|
|
93
|
+
*/
|
|
94
|
+
export function resolveAllExposures(services, context) {
|
|
95
|
+
return services.map((service) => ({
|
|
96
|
+
service,
|
|
97
|
+
paths: resolveExposure(service, context),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=exposure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exposure.js","sourceRoot":"","sources":["../../src/engine/exposure.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk heuristics - composable security rules
|
|
3
|
+
* Each rule returns Finding | null
|
|
4
|
+
*/
|
|
5
|
+
import type { Finding, NetworkContext, ServiceExposure } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Run all heuristics on service exposures
|
|
8
|
+
* Returns findings sorted by severity (critical > warning > info)
|
|
9
|
+
*/
|
|
10
|
+
export declare function analyzeExposures(exposures: ServiceExposure[], context: NetworkContext): Finding[];
|
|
11
|
+
//# sourceMappingURL=heuristics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heuristics.d.ts","sourceRoot":"","sources":["../../src/engine/heuristics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAmB,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE5F;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,eAAe,EAAE,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,EAAE,CA6BjG"}
|