@trohde/earos 1.3.0 → 1.3.2
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.
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "EaROS CLI comprehensive code review: 28 findings across security, performance, architecture, type safety, agent parity"
|
|
3
|
+
category: security-issues
|
|
4
|
+
date: 2026-03-23
|
|
5
|
+
tags:
|
|
6
|
+
- code-review
|
|
7
|
+
- path-traversal
|
|
8
|
+
- typescript-strict-mode
|
|
9
|
+
- dead-code-removal
|
|
10
|
+
- performance-optimization
|
|
11
|
+
- cli-architecture
|
|
12
|
+
- express-server
|
|
13
|
+
- agent-parity
|
|
14
|
+
- type-safety
|
|
15
|
+
- earos-cli
|
|
16
|
+
severity: high
|
|
17
|
+
component:
|
|
18
|
+
- tools/editor/src/serve.ts
|
|
19
|
+
- tools/editor/bin.js
|
|
20
|
+
- tools/editor/src/init.ts
|
|
21
|
+
- tools/editor/src/export-docx.ts
|
|
22
|
+
- tools/editor/manifest-cli.mjs
|
|
23
|
+
- tools/editor/src/utils/validate.ts
|
|
24
|
+
- tools/editor/src/utils/export-markdown.ts
|
|
25
|
+
- tools/editor/src/components/AssessmentSummary.tsx
|
|
26
|
+
- tools/editor/package.json
|
|
27
|
+
- tools/editor/tsconfig.server.json
|
|
28
|
+
root_cause_type: accumulated-tech-debt
|
|
29
|
+
resolution_type: multi-phase-refactor
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# EaROS CLI Comprehensive Code Review — 28 Findings
|
|
33
|
+
|
|
34
|
+
## Problem
|
|
35
|
+
|
|
36
|
+
A 6-agent code review of the EaROS CLI (`tools/editor/`, ~3300 lines TypeScript/JavaScript) identified 28 findings across security, performance, architecture, type safety, agent parity, and code simplification. The CLI had grown organically with no prior comprehensive review, accumulating technical debt across multiple concern areas.
|
|
37
|
+
|
|
38
|
+
**Key symptoms:**
|
|
39
|
+
- Path traversal bypass in `safeRepoPath()` — `startsWith` without trailing separator allowed access to sibling directories
|
|
40
|
+
- Express server bound to `0.0.0.0`, exposing file read/write API to the local network
|
|
41
|
+
- Dead TypeScript source files (`cli.ts`, `manifest-cli.ts`) that were never compiled, creating contributor confusion
|
|
42
|
+
- CLI export command hardcoded to artifact kind only — rubrics and evaluations rejected
|
|
43
|
+
- No structured JSON output for any CLI command, blocking agent/CI integration
|
|
44
|
+
- Sequential icon downloads in `earos init --icons` (3 cloud packages one-at-a-time)
|
|
45
|
+
- `strict: false` in TypeScript configs with 49+ `any` usages
|
|
46
|
+
- Silent validation bypass — schema compilation failure returned `valid: true`
|
|
47
|
+
|
|
48
|
+
## Root Cause
|
|
49
|
+
|
|
50
|
+
Six independent root causes identified:
|
|
51
|
+
|
|
52
|
+
1. **Security:** `safeRepoPath` used `abs.startsWith(repoRoot)` without appending a path separator. On a system where `repoRoot = /projects/EAROS`, the path `/projects/EAROS-evil/secret.yaml` would pass the check. The server's `app.listen(port)` call omitted the host parameter, defaulting to `0.0.0.0`. The `resolveLocalMermaidImage` function had no containment check for `..` segments after the `/icons/` prefix.
|
|
53
|
+
|
|
54
|
+
2. **Dead code:** `src/cli.ts` (78 lines) was described as a "TypeScript source reference for bin.js" but was never in any `tsconfig.json` files array. It had diverged from `bin.js` (missing `init`, `export`, `manifest`, `dev` commands). Similarly, `manifest-cli.ts` existed alongside `manifest-cli.mjs` but was never compiled.
|
|
55
|
+
|
|
56
|
+
3. **Agent parity:** The `earos export` command checked `if (artifactData?.kind !== 'artifact')` and rejected all other kinds. No `--json` flag existed on any command.
|
|
57
|
+
|
|
58
|
+
4. **Performance:** The `for (const config of ICON_PACKAGES)` loop in `init.ts` awaited each download sequentially. The `createIconAliases` function used `.sort()[0]` (O(n log n)) instead of a linear max-scan (O(n)).
|
|
59
|
+
|
|
60
|
+
5. **Type safety:** Both `tsconfig.json` and `tsconfig.server.json` had `strict: false`. The `getGateSeverity` function was duplicated in `AssessmentSummary.tsx` and `score-helpers.tsx`.
|
|
61
|
+
|
|
62
|
+
6. **Silent fail-open:** In `validate.ts`, when `ajv.compile(schema)` threw an error, the catch block returned `null`, and the caller treated `null` as "valid" — meaning a broken schema silently validated everything.
|
|
63
|
+
|
|
64
|
+
## Solution
|
|
65
|
+
|
|
66
|
+
Implemented across 5 phases, published as `@trohde/earos@1.3.0`.
|
|
67
|
+
|
|
68
|
+
### Phase 1 — Security Hardening
|
|
69
|
+
|
|
70
|
+
**Path traversal fix** (the most critical finding):
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// BEFORE (vulnerable)
|
|
74
|
+
function safeRepoPath(repoRoot: string, rawPath: string): string | null {
|
|
75
|
+
const decoded = decodeURIComponent(rawPath)
|
|
76
|
+
const abs = resolve(repoRoot, decoded)
|
|
77
|
+
if (!abs.startsWith(repoRoot)) return null // EAROS-evil passes!
|
|
78
|
+
return abs
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// AFTER (fixed)
|
|
82
|
+
import { sep } from 'path'
|
|
83
|
+
|
|
84
|
+
function safeRepoPath(repoRoot: string, rawPath: string): string | null {
|
|
85
|
+
const decoded = decodeURIComponent(rawPath)
|
|
86
|
+
const abs = resolve(repoRoot, decoded)
|
|
87
|
+
if (abs !== repoRoot && !abs.startsWith(repoRoot + sep)) return null
|
|
88
|
+
return abs
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Other security fixes:**
|
|
93
|
+
- Bound Express to `127.0.0.1` with `EAROS_HOST` env var override for Docker/WSL2
|
|
94
|
+
- Added `X-Content-Type-Options: nosniff` and `X-Frame-Options: DENY` headers
|
|
95
|
+
- Sanitized 500 error responses — no more filesystem path leaks
|
|
96
|
+
- Scoped body limits: 1MB default, 5MB for file writes, 25MB for exports
|
|
97
|
+
- Restricted `POST /api/file/*` to `.yaml`/`.yml` extensions only
|
|
98
|
+
- Added containment check to `resolveLocalMermaidImage` with `baseDir + sep`
|
|
99
|
+
|
|
100
|
+
### Phase 2 — Dead Code Removal
|
|
101
|
+
|
|
102
|
+
- Deleted `src/cli.ts` (78 lines)
|
|
103
|
+
- Deleted `manifest-cli.ts` (diverged from `manifest-cli.mjs`)
|
|
104
|
+
- Updated `manifest-cli.mjs` header comment
|
|
105
|
+
|
|
106
|
+
### Phase 3 — Agent Parity
|
|
107
|
+
|
|
108
|
+
- Extended `earos export` to handle `artifact`, `evaluation`, `core_rubric`, `profile`, and `overlay` kinds
|
|
109
|
+
- Added `--format md` for Markdown export (artifact and rubric)
|
|
110
|
+
- Added `--json` flag to `earos validate` and `earos manifest check`
|
|
111
|
+
- Added `earos manifest list [--json]` command
|
|
112
|
+
- Added `earos list evaluations [--json]` command
|
|
113
|
+
|
|
114
|
+
### Phase 4 — Performance
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// BEFORE: sequential icon downloads
|
|
118
|
+
for (const config of ICON_PACKAGES) {
|
|
119
|
+
const result = await downloadIconPackage(target, config)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// AFTER: parallel with Promise.allSettled
|
|
123
|
+
const settled = await Promise.allSettled(
|
|
124
|
+
ICON_PACKAGES.map(config => downloadIconPackage(target, config))
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Replaced `sort()[0]` with linear max-scan in alias matching
|
|
129
|
+
- Lazy-loaded `ARTIFACT_SCHEMA` via `getArtifactSchema()` function
|
|
130
|
+
|
|
131
|
+
### Phase 5 — Code Quality
|
|
132
|
+
|
|
133
|
+
- Enabled `strict: true` in `tsconfig.server.json` (compiled with zero errors)
|
|
134
|
+
- De-duplicated `getGateSeverity` — `AssessmentSummary.tsx` now imports from `score-helpers.tsx`
|
|
135
|
+
- Moved `mermaid` from `dependencies` to `devDependencies` (Vite bundles it into dist/)
|
|
136
|
+
- Fixed silent validation bypass to fail-closed:
|
|
137
|
+
```typescript
|
|
138
|
+
// BEFORE: catch returns null → caller treats as valid
|
|
139
|
+
} catch { return null }
|
|
140
|
+
|
|
141
|
+
// AFTER: catch returns a validator that always fails
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.warn('[validate] Schema compilation failed:', e)
|
|
144
|
+
const failValidator = Object.assign(
|
|
145
|
+
() => false,
|
|
146
|
+
{ errors: [{ instancePath: '', message: 'Schema compilation failed' }] }
|
|
147
|
+
) as unknown as ReturnType<typeof ajv.compile>
|
|
148
|
+
cache.set(key, failValidator)
|
|
149
|
+
return failValidator
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
- Added 5-second TTL cache for evaluation file endpoint scans
|
|
153
|
+
|
|
154
|
+
## Verification Steps
|
|
155
|
+
|
|
156
|
+
1. **Build:** `npm run build` completed with zero errors under `strict: true`
|
|
157
|
+
2. **CLI commands:** Tested `earos validate` (human + `--json`), `earos export` (all 3 kinds + `--format md`), `earos manifest check --json`, `earos manifest list`, `earos list evaluations --json`
|
|
158
|
+
3. **Browser tests:** Verified home screen, rubric editor (load EAROS-REFARCH-001), assessment wizard (4-step flow), evaluation viewer (19 criteria loaded), security headers present in HTTP responses, path traversal blocked (`../EAROS-evil/` returns 403), YAML-only write restriction enforced
|
|
159
|
+
4. **Published** as `@trohde/earos@1.3.0` via CI pipeline (GitHub Actions)
|
|
160
|
+
|
|
161
|
+
## Prevention Strategies
|
|
162
|
+
|
|
163
|
+
### Path Traversal Prevention
|
|
164
|
+
- **Canonical pattern:** Always append `path.sep` before `startsWith` comparison. Wrap in a single `assertWithinDirectory(base, candidate)` utility.
|
|
165
|
+
- **CI check:** Grep for `.startsWith(` on path variables without adjacent `path.sep` concatenation.
|
|
166
|
+
|
|
167
|
+
### Server Binding
|
|
168
|
+
- **Rule:** Every `app.listen()` must specify an explicit bind address. Single-argument `listen()` calls should fail linting.
|
|
169
|
+
|
|
170
|
+
### Fail-Closed Validation
|
|
171
|
+
- **Rule:** Validation functions must never return "valid" on error. Every `catch` block in a validation path must either re-throw or return a failure result.
|
|
172
|
+
- **Test requirement:** Every validation function needs a test with a corrupt/missing schema asserting failure.
|
|
173
|
+
|
|
174
|
+
### Agent Parity
|
|
175
|
+
- **Design principle:** Every CLI command must support `--json`. This is non-negotiable for agent-native tools.
|
|
176
|
+
- **Test:** Parameterized test asserting `--json` produces valid JSON for every registered command.
|
|
177
|
+
|
|
178
|
+
### TypeScript Strict Mode
|
|
179
|
+
- **CI guardrail:** Verify `strict: true` stays enabled in `tsconfig.json`:
|
|
180
|
+
```bash
|
|
181
|
+
node -e "const c = require('./tsconfig.json'); if (!c.compilerOptions.strict) process.exit(1)"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Dead Code Detection
|
|
185
|
+
- **CI check:** Compare `.ts` files on disk against `tsc --listFiles` output. Any uncompiled `.ts` under `src/` fails the build.
|
|
186
|
+
|
|
187
|
+
## Code Review Checklist (Express + CLI)
|
|
188
|
+
|
|
189
|
+
### Express Server
|
|
190
|
+
- [ ] Every `app.listen()` specifies an explicit bind address
|
|
191
|
+
- [ ] File path validation uses `resolve()` + separator-appended `startsWith`
|
|
192
|
+
- [ ] Error handlers do not leak stack traces or internal paths
|
|
193
|
+
- [ ] Security headers set (`X-Content-Type-Options`, `X-Frame-Options`)
|
|
194
|
+
- [ ] Body parser limits scoped per route based on actual need
|
|
195
|
+
|
|
196
|
+
### CLI Tool
|
|
197
|
+
- [ ] Every command supports `--json` for structured output
|
|
198
|
+
- [ ] Validation failures produce structured error output
|
|
199
|
+
- [ ] No `catch` block silently swallows errors in validation paths
|
|
200
|
+
|
|
201
|
+
### TypeScript / Build
|
|
202
|
+
- [ ] `tsconfig.json` has `strict: true`
|
|
203
|
+
- [ ] Every `.ts` file under `src/` is included in compilation
|
|
204
|
+
- [ ] Shared logic lives in `utils/`, not duplicated across files
|
|
205
|
+
|
|
206
|
+
## Related Documents
|
|
207
|
+
|
|
208
|
+
- **Prior art:** [Site review findings plan](../../docs/plans/2026-03-23-001-refactor-site-review-findings-plan.md) — same review-then-fix methodology applied to the `site/` frontend (26 findings, 8 agents)
|
|
209
|
+
- **Implementation plan:** [CLI review findings plan](../../docs/plans/2026-03-23-002-refactor-cli-review-findings-plan.md) — the detailed 5-phase plan for this work
|
|
210
|
+
- **Review agents used:** Security Sentinel, Performance Oracle, Architecture Strategist, Kieran TypeScript Reviewer, Agent-Native Reviewer, Code Simplicity Reviewer
|
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function startServer(fileArg) {
|
|
|
55
55
|
process.exit(1);
|
|
56
56
|
}
|
|
57
57
|
const app = express();
|
|
58
|
-
app.use(express.json({ limit: '
|
|
58
|
+
app.use(express.json({ limit: '200mb' }));
|
|
59
59
|
app.use((_req, res, next) => {
|
|
60
60
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
61
61
|
res.setHeader('X-Frame-Options', 'DENY');
|