236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# Post-Slack Merge Blockers — Remediation Plan
|
||
|
||
**Status:** Active
|
||
**Created:** 2026-03-13
|
||
**Branch:** `feature/beta-release`
|
||
**Context:** Slack notification provider is functionally complete. Four blockers remain before merge.
|
||
|
||
---
|
||
|
||
## 1. Executive Summary
|
||
|
||
| # | Blocker | Severity | Effort | Fix Available |
|
||
|---|---------|----------|--------|---------------|
|
||
| 1 | Patch coverage short by 15 backend lines (Slack commit) | Medium | ~1 hr | Yes — add unit tests |
|
||
| 2 | 2 HIGH vulnerabilities in Docker image (`binutils`) | Low | None | No — no upstream fix; build-time only |
|
||
| 3 | `anchore/sbom-action` uses Node.js 20 | Medium | Blocked | No — upstream has not released a node24 build |
|
||
| 4 | 13 MEDIUM vulnerabilities in Docker image | Mixed | ~30 min | 1 fixable (`zlib`), 12 unfixable (no upstream fix) |
|
||
|
||
**Bottom line:** Blocker 1 is the only item requiring code changes. Blockers 2/3/4 are environmental and require either upstream fixes, documented risk acceptance, or a single `apk upgrade` in the Dockerfile.
|
||
|
||
---
|
||
|
||
## 2. Blocker 1: Patch Coverage — 15 Uncovered Backend Lines
|
||
|
||
### Methodology
|
||
|
||
Computed by cross-referencing `git diff HEAD~1...HEAD` (Slack commit `26be592f`) against `backend/coverage.txt` using Go's atomic coverage profile. Only non-test `.go` files in the Slack commit are considered.
|
||
|
||
**Totals:** 63 changed source lines, 15 uncovered → 76.2% patch coverage.
|
||
|
||
### Uncovered Lines
|
||
|
||
#### File: `backend/internal/api/handlers/notification_provider_handler.go` (9 lines)
|
||
|
||
| Lines | Code | Description |
|
||
|-------|------|-------------|
|
||
| 141–143 | `return "PROVIDER_TEST_VALIDATION_FAILED", "validation", "Slack rejected the payload..."` | Error classification for `invalid_payload` / `missing_text_or_fallback` Slack API errors |
|
||
| 145–147 | `return "PROVIDER_TEST_AUTH_REJECTED", "dispatch", "Slack webhook is revoked..."` | Error classification for `no_service` Slack API error |
|
||
| 325–327 | `respondSanitizedProviderError(c, http.StatusBadRequest, "TOKEN_WRITE_ONLY", ...)` + `return` | Guard preventing Slack webhook URL from being sent in test-notification requests |
|
||
|
||
#### File: `backend/internal/services/notification_service.go` (6 lines)
|
||
|
||
| Lines | Code | Description |
|
||
|-------|------|-------------|
|
||
| 462–463 | `marshalErr` branch → `return fmt.Errorf("failed to normalize slack payload: %w", marshalErr)` | Error path when `json.Marshal` fails during Slack payload normalization (`message` → `text` rewrite) |
|
||
| 466–467 | `writeErr` branch → `return fmt.Errorf("failed to write normalized slack payload: %w", writeErr)` | Error path when `body.Write` fails after normalization |
|
||
| 549–550 | `return fmt.Errorf("slack webhook URL is not configured")` | Guard when decrypted Slack webhook URL is empty at dispatch time |
|
||
|
||
### Proposed Test Additions
|
||
|
||
All tests go in existing test files alongside the current Slack test cases.
|
||
|
||
**1. `notification_provider_handler_test.go` — Slack error classification (covers lines 141–147)**
|
||
|
||
Add test cases to the `classifySlackProviderTestError` test table:
|
||
- Input error containing `"invalid_payload"` → assert returns `PROVIDER_TEST_VALIDATION_FAILED`
|
||
- Input error containing `"missing_text_or_fallback"` → assert returns `PROVIDER_TEST_VALIDATION_FAILED`
|
||
- Input error containing `"no_service"` → assert returns `PROVIDER_TEST_AUTH_REJECTED`
|
||
|
||
**2. `notification_provider_handler_test.go` — Slack TOKEN_WRITE_ONLY guard (covers lines 325–327)**
|
||
|
||
Add a test case to the test-notification endpoint tests:
|
||
- Send a test-notification request with `type=slack` and a non-empty `token` field
|
||
- Assert HTTP 400 with error code `TOKEN_WRITE_ONLY`
|
||
|
||
**3. `notification_service_test.go` — Slack payload normalization errors (covers lines 462–467)**
|
||
|
||
Add test cases to the Slack dispatch tests:
|
||
- Provide a payload with a `message` field but inject a marshal failure (e.g., via a value that causes `json.Marshal` to fail such as `math.NaN` or a channel-based mock)
|
||
- Alternatively, test the `message`→`text` normalization happy path (which exercises lines 459–467 inclusive) and use a mock `body.Write` that returns an error
|
||
|
||
**4. `notification_service_test.go` — Empty Slack webhook URL (covers lines 549–550)**
|
||
|
||
Add a test case:
|
||
- Create a Slack provider with an empty/whitespace-only Token (webhook URL)
|
||
- Call dispatch
|
||
- Assert error contains `"slack webhook URL is not configured"`
|
||
|
||
---
|
||
|
||
## 3. Blocker 2: 2 HIGH Vulnerabilities
|
||
|
||
### Findings
|
||
|
||
| CVE | Package | Version | CVSS | Fix Available | Source |
|
||
|-----|---------|---------|------|---------------|--------|
|
||
| CVE-2025-69650 | `binutils` | 2.45.1-r0 | 7.5 | No | `grype-results.json` |
|
||
| CVE-2025-69649 | `binutils` | 2.45.1-r0 | 7.5 | No | `grype-results.json` |
|
||
|
||
### Analysis
|
||
|
||
- **CVE-2025-69650:** Double-free in `readelf` when processing crafted ELF files.
|
||
- **CVE-2025-69649:** Null pointer dereference in `readelf` when processing crafted ELF files.
|
||
|
||
Both affect GNU Binutils, which is present in the Alpine image as a build dependency pulled in by `gcc`/`musl-dev` for CGo compilation. These are:
|
||
|
||
1. **Build-time only** — `binutils` is not used at runtime by Charon
|
||
2. **Not exploitable** — requires processing a malicious ELF file via `readelf`, which Charon never invokes
|
||
3. **No upstream fix** — Alpine has not released a patched `binutils`
|
||
4. **Pre-existing** — present before the Slack commit
|
||
|
||
### Remediation
|
||
|
||
- **Action:** Document as accepted risk in the PR description
|
||
- **Rationale:** Build-toolchain-only vulnerability with no runtime exposure. No fix available upstream.
|
||
- **Review trigger:** Re-evaluate when Alpine releases `binutils >= 2.47` or patches the 2.45.1 package
|
||
|
||
---
|
||
|
||
## 4. Blocker 3: `anchore/sbom-action` Uses Node.js 20
|
||
|
||
### Current State
|
||
|
||
| Workflow | File | Line |
|
||
|----------|------|------|
|
||
| Docker Build | `docker-build.yml` | 577 |
|
||
| Nightly Build | `nightly-build.yml` | 266 |
|
||
| Supply Chain PR | `supply-chain-pr.yml` | 269 |
|
||
| Supply Chain Verify | `supply-chain-verify.yml` | 122 |
|
||
|
||
All four reference the same pin: `anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1`
|
||
|
||
**v0.23.1** (released 2026-03-10) is the latest release. Its `action.yml` declares `runs.using: "node20"`.
|
||
|
||
### Analysis
|
||
|
||
- GitHub Actions is deprecating Node.js 20 actions (targeting Node.js 24 as the successor runtime).
|
||
- `anchore/sbom-action` has **not released a node24-compatible version** yet. The project transitioned from node16→node20 around v0.17.x.
|
||
- No open issue or PR on the `anchore/sbom-action` repository tracks node24 migration.
|
||
- The current pin at v0.23.1 / `57aae52` is the best available version.
|
||
|
||
### Remediation
|
||
|
||
| Option | Action | Risk |
|
||
|--------|--------|------|
|
||
| **A) Wait (recommended)** | Keep current pin. Monitor `anchore/sbom-action` releases for a node24 build. Renovate will auto-propose the update. | GitHub will show deprecation warnings but will not break the action until the hard cutoff. |
|
||
| **B) Suppress warning** | Add `ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true` env var to the workflow steps. | Masks the warning but does not fix the underlying issue. Not recommended. |
|
||
| **C) Fork and patch** | Fork the action, change `node20` to `node24`, rebuild dist. | Maintenance burden; likely breaks without code changes to support Node 24 APIs. |
|
||
|
||
**Recommendation:** Option A. Pin stays at `v0.23.1` / `57aae528053a48a3f6235f2d9461b05fbcb7366d`. Document in PR description as a known upstream dependency awaiting update.
|
||
|
||
---
|
||
|
||
## 5. Blocker 4: 13 MEDIUM Vulnerabilities
|
||
|
||
### Full Catalog
|
||
|
||
All from `grype-results.json` (Docker image scan of Alpine 3.23.3 base).
|
||
|
||
| # | CVE | Package | Version | Fix | Category |
|
||
|---|-----|---------|---------|-----|----------|
|
||
| 1 | CVE-2025-60876 | `busybox` | 1.37.0-r30 | None | Unfixable |
|
||
| 2 | CVE-2025-60876 | `busybox-binsh` | 1.37.0-r30 | None | Unfixable (same CVE) |
|
||
| 3 | CVE-2025-60876 | `busybox-extras` | 1.37.0-r30 | None | Unfixable (same CVE) |
|
||
| 4 | CVE-2025-60876 | `ssl_client` | 1.37.0-r30 | None | Unfixable (same CVE) |
|
||
| 5 | CVE-2025-14819 | `curl` | 8.17.0-r1 | None | Unfixable |
|
||
| 6 | CVE-2025-15079 | `curl` | 8.17.0-r1 | None | Unfixable |
|
||
| 7 | CVE-2025-14524 | `curl` | 8.17.0-r1 | None | Unfixable |
|
||
| 8 | CVE-2025-13034 | `curl` | 8.17.0-r1 | None | Unfixable |
|
||
| 9 | CVE-2025-14017 | `curl` | 8.17.0-r1 | None | Unfixable |
|
||
| 10 | CVE-2025-69652 | `binutils` | 2.45.1-r0 | None | Unfixable |
|
||
| 11 | CVE-2025-69644 | `binutils` | 2.45.1-r0 | None | Unfixable |
|
||
| 12 | CVE-2025-69651 | `binutils` | 2.45.1-r0 | None | Unfixable |
|
||
| 13 | CVE-2026-27171 | `zlib` | 1.3.1-r2 | **1.3.2-r0** | **Fixable** |
|
||
|
||
### Grouping
|
||
|
||
| Category | Entries | Unique CVEs | Packages |
|
||
|----------|---------|-------------|----------|
|
||
| BusyBox wget CRLF injection | 4 | 1 (CVE-2025-60876) | busybox, busybox-binsh, busybox-extras, ssl_client |
|
||
| curl TLS/SSH edge cases | 5 | 5 distinct | curl 8.17.0-r1 |
|
||
| binutils readelf issues | 3 | 3 distinct | binutils 2.45.1-r0 |
|
||
| zlib vulnerability | 1 | 1 (CVE-2026-27171) | zlib 1.3.1-r2 |
|
||
|
||
### Remediation
|
||
|
||
**Fixable (1 vuln):**
|
||
|
||
| CVE | Fix |
|
||
|-----|-----|
|
||
| CVE-2026-27171 (`zlib`) | Add `RUN apk upgrade --no-cache zlib` to Dockerfile runtime stage, or bump Alpine base if 3.23.4+ ships with zlib 1.3.2 |
|
||
|
||
**Unfixable (12 vulns):**
|
||
|
||
| Package | Risk | Mitigation |
|
||
|---------|------|------------|
|
||
| `busybox` (CVE-2025-60876) | Low — wget CRLF injection. Charon does not invoke `wget` at runtime. | Accept risk; monitor Alpine. |
|
||
| `curl` (5 CVEs) | Low–Medium — TLS/SSH edge cases. Charon uses Go `net/http`, not `curl`. Present only for health checks. | Accept risk; consider removing `curl` from runtime image. |
|
||
| `binutils` (3 CVEs) | Low — build-time only (`readelf` DoS). Not in runtime path. | Accept risk; monitor Alpine. |
|
||
|
||
---
|
||
|
||
## 6. Commit Slicing Strategy
|
||
|
||
### Decision: 2 PRs
|
||
|
||
| PR | Scope | Files | Validation |
|
||
|----|-------|-------|------------|
|
||
| **PR-1: Coverage + zlib fix** | Unit tests for 15 uncovered Slack lines + `apk upgrade zlib` in Dockerfile | `notification_provider_handler_test.go`, `notification_service_test.go`, `Dockerfile` | `go test ./...`, re-run grype scan, regenerate patch report |
|
||
| **PR-2: Risk acceptance docs** | Document accepted risks for unfixable vulns + SBOM node20 | PR description or `.github/security-accepted-risks.md` | Review-only |
|
||
|
||
### Trigger Reasons
|
||
|
||
- PR-1 is code (tests + Dockerfile) — requires CI
|
||
- PR-2 is documentation/process — review-only, no CI risk
|
||
- Splitting allows PR-1 to merge quickly while risk discussions happen asynchronously
|
||
|
||
### Rollback
|
||
|
||
- **PR-1:** Safe to revert — only adds tests and an `apk upgrade`. No behavioral changes.
|
||
- **PR-2:** Documentation only.
|
||
|
||
---
|
||
|
||
## 7. Execution Order
|
||
|
||
| Step | Action | Blocker | Effort |
|
||
|------|--------|---------|--------|
|
||
| 1 | Add unit tests for 15 uncovered Slack lines | 1 | ~45 min |
|
||
| 2 | Add `apk upgrade --no-cache zlib` to Dockerfile runtime stage | 4 (partial) | ~5 min |
|
||
| 3 | Re-run backend tests + coverage, regenerate patch report | 1 verification | ~10 min |
|
||
| 4 | Re-run Docker image scan (grype/trivy) | 4 verification | ~5 min |
|
||
| 5 | Open PR-1 with test + Dockerfile changes | — | ~10 min |
|
||
| 6 | Document risk acceptance for unfixable vulns + SBOM node20 in PR-2 | 2, 3, 4 | ~15 min |
|
||
|
||
---
|
||
|
||
## Acceptance Criteria
|
||
|
||
- [ ] All 15 uncovered Slack lines have corresponding unit test cases
|
||
- [ ] Backend patch coverage for Slack commit ≥ 95%
|
||
- [ ] `zlib` upgraded to ≥ 1.3.2-r0 in Docker image
|
||
- [ ] Docker image scan shows 0 fixable MEDIUM+ vulnerabilities
|
||
- [ ] Unfixable vulnerabilities documented with risk acceptance rationale
|
||
- [ ] `anchore/sbom-action` node20 status documented; pin unchanged at v0.23.1
|