osctrl-frontend: React admin SPA at frontend/ (round 3 of 3)#815
Merged
Conversation
alvarofraguas
pushed a commit
to alvarofraguas/osctrl
that referenced
this pull request
May 13, 2026
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
alvarofraguas
pushed a commit
to alvarofraguas/osctrl
that referenced
this pull request
May 14, 2026
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
4235626 to
6c0584b
Compare
alvarofraguas
pushed a commit
to alvarofraguas/osctrl
that referenced
this pull request
May 14, 2026
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
6c0584b to
36f4e89
Compare
alvarofraguas
added a commit
to alvarofraguas/osctrl
that referenced
this pull request
May 14, 2026
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
36f4e89 to
8189434
Compare
alvarofraguas
added a commit
to alvarofraguas/osctrl
that referenced
this pull request
May 14, 2026
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
8189434 to
2989a56
Compare
alvarofraguas
added a commit
to alvarofraguas/osctrl
that referenced
this pull request
May 14, 2026
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
2989a56 to
a02a826
Compare
alvarofraguas
added a commit
to alvarofraguas/osctrl
that referenced
this pull request
May 15, 2026
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
a02a826 to
73dbfc7
Compare
Round 3 of 3. Lands the React + TypeScript + Vite SPA under a new `frontend/` directory at the repo root. The SPA is fully separable from the legacy `osctrl-admin` templates — both can run side-by-side during a migration window, and the legacy admin is not touched by this PR. == Tech stack == - React 19 + TypeScript 5 (strict) - Vite 7 (build), @tailwindcss/vite (styling), Tailwind CSS v4 - TanStack Router (typed file-based routing) - TanStack Query 5 (server state, polling + cache) - TanStack Table 8 (headless tables) - react-hook-form 7 + zod 3 (forms + validation) - Radix UI primitives (à la carte, unstyled) - lucide-react (icons; tree-shaken, no emoji) - Monaco editor (lazy-loaded for the osquery / config editor) - Vitest + @testing-library/react + jsdom (component tests) Bundle: ~780KB JS / ~52KB CSS pre-compression; ~214KB JS + ~9KB CSS after gzip. Monaco is code-split into its own chunk so the initial load doesn't pay the editor cost on pages that don't need it. == Pages (covering parity with the legacy admin) == - Login (env picker + creds, pre-auth env list) - Dashboard (cross-env KPIs, per-env tile, agent-version panel, active-queries progress, recently-seen nodes, failed-enroll watch) - Nodes table (paginated, sortable, searchable; quick-filters; 4×24h activity heatmap per row) - Node detail (system info, status logs, result logs, distributed queries, carves, activity tab with interval picker) - Queries list + run form (target selector, Monaco SQL editor with osquery-table autocomplete, expHours) - Query detail (paginated virtual-scroll results, CSV export, search-from-result-cell → SQL-template) - Saved queries (CRUD) - Carves list, run form, detail (archive download) - Tags (env-scoped + global) - Users (list, permissions modal, token modal) - Profile (display name, password change, token refresh) - Environments (list, create, edit) + Monaco-based env config editor (options / schedule / packs / decorators / ATC) with DiffView - Enroll page (per-OS one-liners + downloads) - Audit log (paginated, filtered) - Settings (per-service, typed inputs) == Design system == - Custom osctrl tokens (dark default, full light parity, signal-teal accent #2bc4be / #0a8a85, semantic status colors with icons not color-only). - Density modes (comfortable / compact / dense) via CSS custom properties. - Tabular nums, Inter + Space Grotesk + IBM Plex Mono. - Restrained motion (120–220ms transitions, reduced-motion honored). - Single-accent rule: one signal-teal element active per screen. == Routing == TanStack Router with a file-based tree under `frontend/src/routes/`. The `_app` segment is the authenticated shell that wraps every page behind the AppShell (top bar + side nav + env switcher). Login at `/login/$env` is outside `_app`. == Auth == - HttpOnly cookie session (`osctrl_token`) set by the API on login. - Double-submit CSRF (`osctrl_csrf` cookie + `X-CSRF-Token` header) managed via a thin in-memory token store + request interceptor. - 401 from any endpoint redirects to `/login/$env?next=...`. == Deployment == Three patterns, in `deploy/`: 1. nginx (recommended): `deploy/nginx/frontend.conf.example` shows the production pattern (root + try_files for the SPA, /api/* to osctrl-api, baseline security headers, immutable cache for hashed assets, no-cache for index.html). 2. Docker (`deploy/docker/dockerfiles/Dockerfile-osctrl-frontend`): multi-stage (node:20 → nginx:alpine), single image with the SPA pre-built + nginx pre-configured. 3. Static hosting + CDN: ship `frontend/dist/` to S3/Cloudfront/etc., configure CORS on osctrl-api. The dev compose stack adds an `osctrl-frontend` service that builds the same multi-stage image and serves on :8088 alongside the legacy admin on :8443 — operators can compare side-by-side on the same data. == Make targets == - `make frontend-install` — npm ci - `make frontend-dev` — Vite dev server on :5173 (proxies /api → :8081) - `make frontend-test` — vitest + tsc - `make frontend-build` — produces frontend/dist/ - `make frontend` — install + build (CI / Docker shorthand) == CI == `.github/workflows/frontend-build.yml`: - Pinned action SHAs (matches the existing osctrl convention) - typecheck + tests + build - forbid `dangerouslySetInnerHTML` (CI gate — every node-originating field must be JSX-escaped; future contributors get a build break instead of silent XSS regression) - Uploads dist/ as a build artifact == Test plan == - [x] `npx tsc --noEmit` — clean - [x] `npx vitest run` — 19 files, 92 tests pass - [x] `npm run build` — produces frontend/dist/ cleanly - [x] Backend untouched: `go build ./...`, `go vet ./...`, all 14 Go packages' tests still pass - [x] End-to-end smoke against a Kali docker deployment == What this depends on == Stacked on the previous two PRs: - Security hardening (auth bedrock, CSRF, env secret containment, TLS rate-limit) - API extensions (paginated lists, stats, saved-queries CRUD, user/permissions/tokens, env config PATCHes, audit-log filters) When those merge, this branch will be re-targeted at the new main HEAD with no conflicts.
Consolidated follow-up that lands on top of the three stacked PRs to address lint, a real bug uncovered by lint, a stronger JWT-secret contract, and a few deployment-correctness items. == Lint cleanup (golangci-lint on PR jmpsec#815) == - pkg/auditlog/audit.go, pkg/dbutil/buckets.go: drop the redundant `.Dialector` selector on `*gorm.DB` (QF1008). `Dialector` is an embedded interface so the promoted `Name()` works directly. - cmd/api/handlers/utils.go: remove the unused `postgresQueryLogs` function (unused). Pre-existing dead code that surfaced once the package was touched by other PRs in the stack. - cmd/admin/handlers/json-nodes.go: annotate the two legacy admin callers of `Nodes.SearchByEnvPage` / `Nodes.GetByEnvPage` with `//nolint:staticcheck // SA1019: intentional legacy admin caller; new SPA uses GetByEnvPaged`. The deprecation tag is correct — the legacy admin will migrate to `GetByEnvPaged` when it adopts the SPA's pagination shape; until then these calls are gated by the package-layer `SortableColumns` allowlist and are safe. == Real bug uncovered by ineffassign == cmd/api/handlers/environments.go: in the `"create"` action, the tag-creation failure path set `msgReturn = fmt.Sprintf("error generating tag %s ", err.Error())` and then `return`-ed without ever writing to the response. Result: the API returned the request body's buffered HTTP 200 (or no body at all) on a real failure, masking the error from the client. Replaced with a proper `apiErrorResponse(w, "error generating tag", http.StatusInternalServerError, err)`. == JWT secret contract: decouple user-manager construction from token-signing config == Round 1 added `MinJWTSecretBytes = 32` to `users.CreateUserManager`, which `log.Fatal`s when the JWT secret is shorter than 32 bytes. This was correct for the API and admin services (they sign tokens) but caught the CLI (`cmd/cli`) by surprise — the CLI doesn't mint JWTs at all, it just manages user/permission rows directly, and was passing `appName` ("osctrl", 6 bytes) as a placeholder. Every CLI invocation with `--db` would have aborted with "JWT Secret too short" once Round 1 lands. Fix: split the constructor. // DB-only constructor — never validates JWT. func CreateUserManager(backend *gorm.DB) *UserManager // Attach JWT signing config; validates the secret here. func (u *UserManager) WithJWT(*config.YAMLConfigurationJWT) *UserManager Token-issuing callers chain: apiUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) adminUsers = users.CreateUserManager(db.Conn).WithJWT(flagParams.JWT) The CLI calls only `CreateUserManager(db.Conn)`. `CreateToken` now returns an error if invoked on a manager without `WithJWT` — defense-in-depth so a future caller can't accidentally sign tokens with a nil config. == TLS handler: per-endpoint body caps == cmd/tls/handlers/post.go: wrap every osquery-facing `io.ReadAll` in `http.MaxBytesReader` with an endpoint-appropriate ceiling. Without this, a misbehaving or hostile node can submit an arbitrarily large body and force the server to buffer it before parsing. Caps are chosen against real osquery payload sizes: enroll: 64 KiB config: 64 KiB log: 100 MiB (osquery log batches) query read: 16 KiB query write: 100 MiB carve init: 8 KiB carve block: 16 MiB quick-enroll: 8 KiB flags / cert: 8 KiB verify / script: 8 KiB osquery config: 2 MiB A single `readBody(w, r, max)` helper applies the cap and reads, so the call sites stay one-line. == Carve compression: out-of-bounds panic guard == pkg/carves/utils.go: `CheckCompressionRaw` previously dereferenced `data[:4]` to compare against the zstd header. A truncated or empty block (which an authenticated CarveLevel client can submit) would panic with index-out-of-range. Guarded with a length check. == Frontend (PR jmpsec#815 scope) == - frontend/index.html: removed the inline `<script>` that bootstraps the theme attribute and moved it to `frontend/public/theme-bootstrap.js`. The inline form violated the CSP `script-src 'self' blob:` deployed on the SPA (45 CSP errors per page load in the prior audit). - frontend/package.json (+ package-lock.json): add an `overrides` block pinning `dompurify@^3.4.3` to remediate the transitive vulnerability advisory. - frontend/src/features/dashboard/DashboardPage.tsx: wire the time-series chart to real `getEnvActivity` data instead of the placeholder constants. Added a 7d / 24h interactive toggle and an `aggregateBuckets` helper that collapses the 15-min server buckets into N display bins. == Dev stack == deploy/docker/conf/osquery/entrypoint.sh: pin `--host_identifier=specified --specified_identifier=$(hostname)` so the three dev osquery containers enroll as distinct nodes instead of colliding on the host kernel UUID. The full Kali docker stack now enrolls four unique nodes (the Kali host + three containers). == Toolchain == Bump Go from 1.26.1 to 1.26.3 across go.mod, the four deploy/docker/dockerfiles/Dockerfile-dev-*, deploy/lib.sh, .env.example, the .github/actions/{build,test}/binaries action manifests, and the five .github/workflows/*.yml. 1.26.3 carries the stdlib CVE fixes flagged by the local govulncheck run. Verified locally: - go build ./... (clean on Go 1.26.3) - go test ./... (all packages green, including new CreateUserManager / WithJWT tests) - frontend: vitest 92/92 green; npm run build clean; npm audit --omit=dev → 0 vulnerabilities - Live smoke on the Kali dev stack: container rebuild + recreate; osctrl-cli runs `env show`, `node-actions secret`, `show-flags` without the old JWT-too-short fatal; osctrl-api signs a real login token through the SPA; the four enrolled osquery nodes remain in the DB.
73dbfc7 to
b36e3b2
Compare
osquery agents back off on 413 Payload Too Large but not on 500 Internal Server Error. A misbehaving (or hostile) client whose log batch exceeds the per-endpoint cap would previously get a 500, keep retrying with the same payload, and the operator would see a flood of 500s in audit logs with no useful signal. readBody now distinguishes the two cases: - http.MaxBytesError → write 413 directly, return the error - any other read error → return the error untouched (caller writes 500) Callers gain one small isMaxBytesError(err) guard so they skip writing a second status when the helper already wrote 413; a duplicate WriteHeader is a no-op at the wire but logs a noisy warning. All 12 readBody call sites updated. Verified end-to-end against osctrl-tls behind nginx: 100 KiB POST to /enroll (cap 64 KiB) returns HTTP 413; small invalid-secret body still returns HTTP 403.
javuto
approved these changes
May 16, 2026
Collaborator
javuto
left a comment
There was a problem hiding this comment.
Amazing! Nicely done 👏 👏 👏
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Round 3 of 3. Lands the React + TypeScript + Vite SPA under a new
frontend/directory at the repo root. The SPA fully covers what the legacyosctrl-admintemplates do — every page surface is replicated. Both UIs can run side-by-side during a migration window (dev compose serves the SPA on:8088while the legacy admin stays on:8443); the legacy admin is not touched by this PR.mainHEAD with no conflicts.End-to-end tested against a Kali docker deployment.
What's in
frontend/Tech stack
@tailwindcss/vite+ Tailwind CSS v4Bundle: ~780 KB JS / ~52 KB CSS pre-compression → ~214 KB JS + ~9 KB CSS after gzip. Monaco is code-split into its own chunk so pages that don't use it don't pay the editor cost.
Pages
Every page surface the legacy admin offers is covered:
/login/environments)Design system
Locked tokens, captured in
frontend/src/styles/tokens.cssandfrontend/src/lib/design-tokens.ts(kept in sync):data-theme="dark|light"on<html>#2bc4bedark /#0a8a85light), one accent active per screenAuth flow
osctrl_tokencookie set by the API on login (no token inlocalStorage)osctrl_csrfcookie +X-CSRF-Tokenheader) for mutating requests/login/$env?next=...Deployment
Three patterns, all reference each other for consistency:
deploy/nginx/frontend.conf.exampleshows the production pattern:root+try_filesfor the SPA,/api/*toosctrl-api, baseline security headers (HSTS / CSP / X-CTO / XFO / Referrer-Policy / Permissions-Policy), immutable cache for hashed assets, no-cache forindex.html.deploy/docker/dockerfiles/Dockerfile-osctrl-frontend: multi-stage (node:20buildsdist/,nginx:alpineserves it + reverse-proxies/api/*). Single image, single binary's worth of operational surface.frontend/dist/to S3/Cloudfront/etc., configure CORS on osctrl-api.The dev compose stack adds an
osctrl-frontendservice that builds the same multi-stage image on:8088alongside the legacy admin on:8443so operators can compare the two on the same data.Make targets
make frontend-installnpm cimake frontend-dev:5173, proxies/api→:8081make frontend-testmake frontend-buildfrontend/dist/make frontendCI
.github/workflows/frontend-build.yml:npm run check→tsc --noEmit)npm test→ vitest)npm run build→ vite)dangerouslySetInnerHTMLgate: build fails if it appears anywhere undersrc/. Every node-originating field must be JSX-escaped — this gate prevents a future contributor from silently regressing the XSS surface.frontend/dist/as a 7-day artifactTest plan
npx tsc --noEmit— cleannpx vitest run— 19 test files, 92 tests passnpm run build— producesfrontend/dist/cleanlygo build ./...,go vet ./..., all 14 Go packages' tests passWhy a separate
frontend/directorycmd/*next to the Go binaries.working-directory: frontendand can evolve independently of the Go workflows.