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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +220 -0
  3. package/dist/cli/commands/scan.d.ts +5 -0
  4. package/dist/cli/commands/scan.d.ts.map +1 -0
  5. package/dist/cli/commands/scan.js +47 -0
  6. package/dist/cli/commands/scan.js.map +1 -0
  7. package/dist/cli/index.d.ts +6 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +23 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/engine/context.d.ts +6 -0
  12. package/dist/engine/context.d.ts.map +1 -0
  13. package/dist/engine/context.js +33 -0
  14. package/dist/engine/context.js.map +1 -0
  15. package/dist/engine/exposure.d.ts +22 -0
  16. package/dist/engine/exposure.d.ts.map +1 -0
  17. package/dist/engine/exposure.js +100 -0
  18. package/dist/engine/exposure.js.map +1 -0
  19. package/dist/engine/heuristics.d.ts +11 -0
  20. package/dist/engine/heuristics.d.ts.map +1 -0
  21. package/dist/engine/heuristics.js +219 -0
  22. package/dist/engine/heuristics.js.map +1 -0
  23. package/dist/engine/types.d.ts +46 -0
  24. package/dist/engine/types.d.ts.map +1 -0
  25. package/dist/engine/types.js +5 -0
  26. package/dist/engine/types.js.map +1 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +10 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/system/collectors/cloudflare.d.ts +8 -0
  32. package/dist/system/collectors/cloudflare.d.ts.map +1 -0
  33. package/dist/system/collectors/cloudflare.js +32 -0
  34. package/dist/system/collectors/cloudflare.js.map +1 -0
  35. package/dist/system/collectors/firewall.d.ts +10 -0
  36. package/dist/system/collectors/firewall.d.ts.map +1 -0
  37. package/dist/system/collectors/firewall.js +69 -0
  38. package/dist/system/collectors/firewall.js.map +1 -0
  39. package/dist/system/collectors/network.d.ts +10 -0
  40. package/dist/system/collectors/network.d.ts.map +1 -0
  41. package/dist/system/collectors/network.js +18 -0
  42. package/dist/system/collectors/network.js.map +1 -0
  43. package/dist/system/collectors/services.d.ts +6 -0
  44. package/dist/system/collectors/services.d.ts.map +1 -0
  45. package/dist/system/collectors/services.js +31 -0
  46. package/dist/system/collectors/services.js.map +1 -0
  47. package/dist/system/collectors/tailscale.d.ts +10 -0
  48. package/dist/system/collectors/tailscale.d.ts.map +1 -0
  49. package/dist/system/collectors/tailscale.js +39 -0
  50. package/dist/system/collectors/tailscale.js.map +1 -0
  51. package/dist/system/parsers/ip-parser.d.ts +25 -0
  52. package/dist/system/parsers/ip-parser.d.ts.map +1 -0
  53. package/dist/system/parsers/ip-parser.js +100 -0
  54. package/dist/system/parsers/ip-parser.js.map +1 -0
  55. package/dist/system/parsers/ss-parser.d.ts +15 -0
  56. package/dist/system/parsers/ss-parser.d.ts.map +1 -0
  57. package/dist/system/parsers/ss-parser.js +153 -0
  58. package/dist/system/parsers/ss-parser.js.map +1 -0
  59. package/dist/system/shell.d.ts +27 -0
  60. package/dist/system/shell.d.ts.map +1 -0
  61. package/dist/system/shell.js +56 -0
  62. package/dist/system/shell.js.map +1 -0
  63. package/dist/ui/findings.d.ts +6 -0
  64. package/dist/ui/findings.d.ts.map +1 -0
  65. package/dist/ui/findings.js +91 -0
  66. package/dist/ui/findings.js.map +1 -0
  67. package/dist/ui/renderer.d.ts +18 -0
  68. package/dist/ui/renderer.d.ts.map +1 -0
  69. package/dist/ui/renderer.js +33 -0
  70. package/dist/ui/renderer.js.map +1 -0
  71. package/dist/ui/summary.d.ts +17 -0
  72. package/dist/ui/summary.d.ts.map +1 -0
  73. package/dist/ui/summary.js +39 -0
  74. package/dist/ui/summary.js.map +1 -0
  75. package/dist/ui/theme.d.ts +39 -0
  76. package/dist/ui/theme.d.ts.map +1 -0
  77. package/dist/ui/theme.js +47 -0
  78. package/dist/ui/theme.js.map +1 -0
  79. 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,5 @@
1
+ /**
2
+ * Scan command - orchestrates the security scan
3
+ */
4
+ export declare function scanCommand(): Promise<void>;
5
+ //# sourceMappingURL=scan.d.ts.map
@@ -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,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Safecrab CLI entry point
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=index.d.ts.map
@@ -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,6 @@
1
+ /**
2
+ * Build NetworkContext from system collectors
3
+ */
4
+ import type { NetworkContext } from "./types.js";
5
+ export declare function buildNetworkContext(): Promise<NetworkContext>;
6
+ //# sourceMappingURL=context.d.ts.map
@@ -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"}