Skip to content

Commit 50a77a2

Browse files
committed
Merge branch 'main' into feature/news-resilience
2 parents eba138e + 7136aa6 commit 50a77a2

4 files changed

Lines changed: 394 additions & 13 deletions

File tree

CLAUDE.md

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ SAP_DEVS_DEV=1 go run . inject --dry-run
2525

2626
> **Windows note:** `go test` always fails locally due to Windows Defender blocking binary execution from `.config` paths. Use `go build` + `go vet` locally; CI is the authoritative test runner.
2727
28+
## Gotchas
29+
30+
- **`go test` fails on Windows** — Windows Defender blocks test binaries from `~/.config` paths. Use `go build ./...` + `go vet ./...` locally; CI (`ubuntu-latest`) is the authoritative test runner.
31+
- **Tray binary has its own `go.mod`**`cmd/sap-devs-tray/` is a separate Go module. The main CLI must never import Wails. Build it separately with `CGO_ENABLED=1`.
32+
- **Worktrees live in `.worktrees/`** — not in `~/.config` (Windows Defender). Use `git worktree add .worktrees/<name> -b feature/<name>`.
33+
- **`sap-devs inject` manages part of this file** — the `<!-- sap-devs:start -->` block at the bottom is auto-generated. Don't edit it manually; run `sap-devs inject` to refresh.
34+
- **Content loaded from `./content/` in dev mode** — set `SAP_DEVS_DEV=1` to bypass the user cache during development.
35+
- **`context.expanded.md` takes precedence** — if present alongside `context.md`, `inject` uses the expanded version (generated by `sync` from `<!-- sap-devs:fetch -->` markers).
36+
37+
## Code Style
38+
39+
- **gofmt enforced** — the `.claude/settings.json` PostToolUse hook auto-formats every `.go` edit. No exceptions.
40+
- **`go vet` on every edit** — also wired as a PostToolUse hook; catches issues immediately.
41+
- **i18n keys** — CLI strings use `i18n.T(lang, key)` with keys in `internal/i18n/catalogs/en.json`. Convention: `<command>.<subcommand>.<string_name>`.
42+
- **Error handling** — return `fmt.Errorf(...)` from `RunE`; cobra prints to stderr. Don't `os.Exit()` in command bodies.
43+
- **No CGO in main CLI** — all platform-specific code uses build tags + pure Go. CGO is only for the tray binary.
44+
2845
## Architecture Overview
2946

3047
This is a Go CLI built with [cobra](https://github.com/spf13/cobra). Its core purpose is to inject SAP developer knowledge into AI coding tools (Claude Code, Cursor, Copilot, etc.) and wire up SAP MCP servers.
@@ -72,25 +89,15 @@ Profiles ([content/profiles/](content/profiles/)) are YAML files that tag which
7289

7390
### Discovery Center
7491

75-
`sap-devs discovery` ([cmd/discovery.go](cmd/discovery.go), [cmd/discovery_services.go](cmd/discovery_services.go), [cmd/discovery_guidance.go](cmd/discovery_guidance.go)) surfaces content from the SAP Discovery Center via two OData V2 services. Curated references in `discovery.yaml` per pack are enriched with live API data cached at `~/.cache/sap-devs/discovery/` (7-day TTL). The `internal/discovery` package ([internal/discovery/client.go](internal/discovery/client.go)) handles CSRF tokens, OData `$batch` requests, and the double-JSON-encoding quirk of the `/platformx/` endpoint.
76-
77-
Three content types: **missions** (guided learning paths), **services** (BTP service catalog), and **guidance** (BTP Guidance Framework phases). Profile-aware filtering uses `profile_filters` in `discovery.yaml` to auto-filter by product/category/focus tags.
92+
`sap-devs discovery` ([cmd/discovery.go](cmd/discovery.go)) surfaces missions, BTP services, and guidance from the SAP Discovery Center via OData V2 APIs. Curated references in `discovery.yaml` per pack are enriched with live data cached at `~/.cache/sap-devs/discovery/` (7-day TTL). Profile-aware filtering uses `profile_filters` in `discovery.yaml`. See [docs/developer/developer-guide.md](docs/developer/developer-guide.md) for OData client details.
7893

7994
### Tutorials
8095

81-
`sap-devs tutorial` ([cmd/tutorials.go](cmd/tutorials.go)) fetches and renders tutorials from the `sap-tutorials` GitHub organization (~1,290 tutorials across ~21 repos). The `internal/tutorials` package handles the GitHub client, markdown parsing (v2 H3-steps and legacy ACCORDION format), full-text search, caching, and per-user progress tracking.
82-
83-
**Sync:** Tutorial metadata is synced as an independent category (`tutorials`) during `sap-devs sync`. A two-phase pipeline fetches repo metadata + tree listings via the GitHub API (bounded to 5 concurrent), then fetches YAML frontmatter from the CDN (bounded to 10 concurrent). Incremental sync skips repos whose tree SHA hasn't changed. Full tutorial content is fetched on demand by `tutorial show`.
84-
85-
**Pack integration:** Each pack can include a `tutorials.yaml` file with curated `TutorialRef` entries (slug + featured flag). `tutorial list` joins these refs with the cached index for profile-filtered browsing; `tutorial search` searches the full index across all repos.
96+
`sap-devs tutorial` ([cmd/tutorials.go](cmd/tutorials.go)) fetches and renders tutorials from the `sap-tutorials` GitHub organization (~1,290 tutorials across ~21 repos). Metadata is synced incrementally during `sap-devs sync`; full content is fetched on demand by `tutorial show`. Each pack can include `tutorials.yaml` with curated refs. See [docs/developer/developer-guide.md](docs/developer/developer-guide.md) for sync pipeline details.
8697

8798
### Learning Journeys
8899

89-
`sap-devs learning` ([cmd/learning.go](cmd/learning.go)) browses SAP Learning Journeys from learning.sap.com. The `internal/learning` package handles catalog download, caching, local search, and the search API client.
90-
91-
**Sync:** The full catalog JSON (~5.4MB, ~5,100 items) is fetched from `learning.sap.com/service/catalog-download/json` during `sap-devs sync`, filtered to `Learning_type == "Learning Journey"` (~351 items), and cached at `~/.cache/sap-devs/learning/index.json` (7-day TTL). The `search` subcommand uses the `getCards` search API for server-side fuzzy matching, with results cached at 1-hour TTL. Falls back to local substring search if the API is unreachable.
92-
93-
**Pack integration:** Each pack can include a `learning.yaml` file with `profile_filters` (products, product_categories, roles) and curated `LearningRef` entries (slug + featured flag). `learning list` uses a three-tier resolution algorithm: featured refs first, then pack-referenced refs, then profile-filtered journeys from the full index. Featured journeys are also injected into the AI context during `inject`.
100+
`sap-devs learning` ([cmd/learning.go](cmd/learning.go)) browses SAP Learning Journeys from learning.sap.com. The full catalog (~351 learning journeys) is synced during `sap-devs sync` and cached at `~/.cache/sap-devs/learning/index.json` (7-day TTL). Each pack can include `learning.yaml` with `profile_filters` and curated refs. Featured journeys are injected into AI context during `inject`. See [docs/developer/developer-guide.md](docs/developer/developer-guide.md) for API client details.
94101

95102
### Learn
96103

@@ -203,6 +210,15 @@ Releases use GoReleaser triggered by `v*` tags. The binary is named `sap-devs`.
203210

204211
Git worktrees for feature branches are stored in `.worktrees/` in the project root (not in `~/.config` — Windows Defender blocks test binary execution from that path).
205212

213+
### Claude Code Automations
214+
215+
The `.claude/` directory and `.mcp.json` are checked in. Contributors get these automatically:
216+
217+
- **Hooks** (`.claude/settings.json`): `gofmt -w` and `go vet ./...` run on every `.go` file edit via PostToolUse hooks.
218+
- **Subagents** (`.claude/agents/security-reviewer.md`): Security-focused reviewer for credentials, binary downloads, OS services, and HTTP clients. Invoke with `@security-reviewer`.
219+
- **Skills** (`.claude/skills/release-notes/`): `/release-notes` generates GitHub Release notes from commits since the last tag.
220+
- **MCP Servers** (`.mcp.json`): context7 for live Go library documentation (cobra, bubbletea, mcp-go, Wails v3).
221+
206222
<!-- sap-devs:start:SAP Developer Context -->
207223
# SAP Developer Context
208224

docs/developer/developer-guide.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,55 @@ Feature branch worktrees are stored in `.worktrees/` in the project root — **n
491491
# Create a worktree for a feature branch
492492
git worktree add .worktrees/my-feature -b feature/my-feature
493493
```
494+
495+
---
496+
497+
## Claude Code Setup
498+
499+
The project ships with Claude Code automations in `.claude/` and `.mcp.json`. These are checked in so every contributor gets the same setup.
500+
501+
### Hooks (`.claude/settings.json`)
502+
503+
Two PostToolUse hooks run automatically after every Edit/Write of a `.go` file:
504+
505+
| Hook | What it does |
506+
|------|-------------|
507+
| **gofmt** | Auto-formats the edited file with `gofmt -w` |
508+
| **go vet** | Runs `go vet ./...` and shows the first 20 lines of output |
509+
510+
These replace the need to remember to format or lint — every Go edit is immediately cleaned up.
511+
512+
> **Why not `go test`?** Windows Defender blocks test binary execution from `~/.config` paths. `go vet` is the local quality gate; CI is the authoritative test runner.
513+
514+
### MCP Servers (`.mcp.json`)
515+
516+
| Server | Purpose |
517+
|--------|---------|
518+
| **context7** | Live documentation lookup for Go libraries (cobra, bubbletea, mcp-go, Wails v3, etc.) |
519+
520+
context7 gives Claude access to current library documentation instead of relying on training data. This is especially valuable for Wails v3 (alpha API that changes frequently) and mcp-go.
521+
522+
### Subagents (`.claude/agents/`)
523+
524+
| Agent | Purpose |
525+
|-------|---------|
526+
| **security-reviewer** | Security-focused code review for credential handling, binary downloads, OS services, and HTTP clients |
527+
528+
Invoke with: `@security-reviewer review the changes in internal/credentials/`
529+
530+
The security reviewer focuses on the areas documented in [security-review.md](security-review.md) and reports findings by severity (CRITICAL/HIGH/MEDIUM/LOW).
531+
532+
### Skills (`.claude/skills/`)
533+
534+
| Skill | Invocation | Purpose |
535+
|-------|-----------|---------|
536+
| **release-notes** | `/release-notes` | Generate release notes from commits since the last tag, grouped by conventional commit type |
537+
538+
### Plugin Recommendations
539+
540+
These are not checked in but recommended for individual developer setup:
541+
542+
| Plugin | Install | Purpose |
543+
|--------|---------|---------|
544+
| **gopls-lsp** | `/plugin install gopls-lsp` | Go language server — go-to-definition, find-references, hover for the full codebase |
545+
| **commit-commands** | `/plugin install commit-commands` | `/commit` and `/commit-push-pr` slash commands |

docs/developer/security-review.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Security Review
2+
3+
Last reviewed: 2026-04-20
4+
5+
## Scope
6+
7+
Full codebase review covering:
8+
9+
- Embedded HTTP server with session token auth (tray binary)
10+
- OS-native scheduler registration (Windows/Linux/macOS)
11+
- ZIP archive extraction from GitHub
12+
- Credential handling (keychain + file fallback)
13+
- Content loading from multiple directory layers
14+
- Static file serving via embedded filesystem
15+
- External API interactions (Discovery Center, GitHub, YouTube RSS, ip-api.com)
16+
- Autostart registration (LaunchAgent plist, .desktop files, Windows Registry)
17+
- Subprocess execution across the CLI
18+
19+
## Methodology
20+
21+
Three-phase analysis:
22+
23+
1. Broad vulnerability scan across all security-critical areas
24+
2. Per-finding deep-dive validation by independent reviewers
25+
3. False-positive filtering against confidence threshold (>=80%)
26+
27+
Categories examined: input validation, authentication/authorization, crypto/secrets management, injection/code execution, data exposure, path traversal, and deserialization.
28+
29+
## Findings
30+
31+
No vulnerabilities met the high-confidence reporting threshold.
32+
33+
## Areas Investigated
34+
35+
### ZIP Extraction (`internal/sync/fetcher.go`)
36+
37+
**Check:** Zip slip attack via `../` entries in downloaded archives.
38+
39+
**Result:** Protected. The extraction code normalizes the destination with `filepath.Abs`, cleans entry paths with `filepath.Join` + `filepath.FromSlash`, and validates with a `strings.HasPrefix` check including the path separator. A dedicated test (`TestFetcher_BlocksZipSlip`) verifies the guard.
40+
41+
### Tray Binary Extraction (`internal/trayctl/extract.go`)
42+
43+
**Check:** Path traversal during tar.gz/zip extraction of the tray companion binary.
44+
45+
**Result:** Safe by design. Archive contents are extracted into memory only (via `io.ReadAll`), then written to a single controlled path returned by `Manager.BinaryPath()`. No intermediate filesystem operations are exposed.
46+
47+
### HTTP Static File Server (`cmd/sap-devs-tray/server.go`)
48+
49+
**Check:** Directory traversal via requests like `../../etc/passwd`.
50+
51+
**Result:** Safe. Static assets are served from a Go `embed.FS`, which is fundamentally bounded to the files embedded at compile time. There is no access to the real filesystem.
52+
53+
### Session Token Auth (`cmd/sap-devs-tray/server.go`)
54+
55+
**Check:** Token leakage via URL query parameters, Referrer headers, or browser history.
56+
57+
**Result:** Acceptable for the threat model. The token is passed in query parameters within a Wails v3 embedded webview (not a regular browser). Mitigating factors:
58+
59+
- Wails webview cannot navigate to external URLs — no Referrer leakage
60+
- All resources are embedded — no CDN or external font/image loads
61+
- Server binds to `127.0.0.1` only — no network exposure
62+
- Token is 128-bit, generated via `crypto/rand` — cryptographically secure
63+
- Token is per-process and regenerated on each app restart
64+
65+
### OS Scheduler Registration (`internal/service/scheduler_*.go`)
66+
67+
**Check:** Shell command injection via the binary path embedded in scheduled task definitions.
68+
69+
**Result:** Not exploitable. The binary path originates exclusively from `os.Executable()` (resolved through `filepath.EvalSymlinks`), which is system state — not user input. Exploitation would require an attacker to control where the binary is installed, which already grants direct code execution without needing shell injection. Platform-specific notes:
70+
71+
- **Windows:** Double-quoted paths in `cmd /c` cannot be broken by valid Windows filenames (which prohibit `"`)
72+
- **Linux:** Path in double quotes inside single quotes in the systemd unit; breaking out requires a `'` in the path, but an attacker with that level of filesystem control can replace the binary directly
73+
- **macOS:** Similar to Linux; plist passes the command string to `/bin/sh -c`
74+
75+
### Autostart Registration (`internal/trayctl/autostart.go`)
76+
77+
**Check:** XML injection (macOS plist) and desktop entry injection (Linux `.desktop` file) via unescaped binary path.
78+
79+
**Result:** Not exploitable. Binary path comes from `os.Executable()`. Additionally:
80+
81+
- macOS: launchd's XML parser treats `<string>` values as literal data, not evaluated for entities
82+
- Linux: The `Exec=` field in desktop entries is parsed by the desktop environment via direct exec, not shell-evaluated per the freedesktop specification
83+
84+
### Content Loader (`internal/content/loader.go`)
85+
86+
**Check:** Path traversal via pack IDs or content paths, including the project layer (`.sap-devs/`).
87+
88+
**Result:** Safe. `os.ReadDir` returns only immediate child entries (names cannot contain path separators on any filesystem). The project directory is hardcoded to `.sap-devs/` joined with the current working directory — no user-controllable path components.
89+
90+
### Credentials (`internal/credentials/credentials.go`)
91+
92+
**Check:** Token exposure in logs, error messages, or insecure file storage.
93+
94+
**Result:** Safe across all layers of the credential lifecycle.
95+
96+
**Storage:** Primary storage uses the OS keychain via `zalando/go-keyring` (macOS Keychain, Windows Credential Manager, Linux Secret Service). When the keychain is unavailable, the fallback writes to `<configDir>/credentials` with `0600` permissions (owner read/write only).
97+
98+
**No token leakage in logs.** The three `fmt.Fprintf(os.Stderr, ...)` calls in the credentials package only log the keychain error message (e.g., "keychain unavailable: ..."), never the token value itself.
99+
100+
**Token masked in CLI output.** The `config show` command uses a `maskedToken()` helper (`cmd/config.go:71-83`) that displays "set" or "not set" — the actual token value is never printed.
101+
102+
**Shell history warning.** When the user passes a token as a CLI argument (`sap-devs config token <value>`), the code explicitly warns about shell history exposure and suggests using the interactive prompt instead (`cmd/config.go:191-192`).
103+
104+
**Resolution chain is clean.** `Resolve()` checks environment variables (`GH_TOKEN`, `GITHUB_TOKEN`, `GITHUB_TOOLS_SAP_TOKEN`) then keychain/file — the resolved token is never logged or included in error messages at any point in the chain.
105+
106+
**Credential file naming.** The `credFileForUser` function builds the fallback filename from the `service` parameter (e.g., `credentials-youtube`). This value comes from the `--service` CLI flag, which is trusted input. `filepath.Join` normalizes the path, preventing traversal.
107+
108+
### Environment Variable Execution (`TERMINAL`, `PAGER`)
109+
110+
**Check:** Command injection via `$TERMINAL` or `$PAGER` environment variables.
111+
112+
**Result:** Excluded from scope. Environment variables are trusted values in this security model — an attacker who can modify them already has equivalent access to the user's session.
113+
114+
## Positive Security Observations
115+
116+
| Area | Implementation |
117+
|------|---------------|
118+
| Zip slip protection | Explicit `HasPrefix` check with path separator, backed by unit test |
119+
| Session tokens | 128-bit via `crypto/rand.Read` + `hex.EncodeToString` |
120+
| HTTP server binding | Hardcoded to `127.0.0.1:0` (loopback, random port) |
121+
| Static asset serving | Go `embed.FS` eliminates directory traversal by design |
122+
| Binary path resolution | `filepath.EvalSymlinks` before use in scheduler/autostart |
123+
| Subprocess arguments | `exec.Command` with separate argument strings (not shell-concatenated) |
124+
| Archive size limits | `io.LimitReader` applied during tray binary extraction |
125+
| Credential file permissions | `0600` on fallback credential file |

0 commit comments

Comments
 (0)