Skip to content

Commit ef3a7c0

Browse files
authored
Merge pull request #133 from jackby03/feat/125-serve
feat(serve): hardbox serve — lightweight web dashboard
2 parents 4f8da1e + 3509379 commit ef3a7c0

File tree

8 files changed

+742
-0
lines changed

8 files changed

+742
-0
lines changed

docs/SERVE.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# hardbox serve — Web Dashboard
2+
3+
`hardbox serve` starts a local, read-only HTTP dashboard for browsing audit reports, inspecting findings, and comparing reports side by side.
4+
5+
## Usage
6+
7+
```bash
8+
hardbox serve [flags]
9+
```
10+
11+
### Flags
12+
13+
| Flag | Default | Description |
14+
|---|---|---|
15+
| `--reports-dir` | `.` | Directory containing JSON audit reports |
16+
| `--port` | `8080` | Port to listen on (ignored when `--addr` is set) |
17+
| `--addr` | `127.0.0.1:8080` | Full listen address — override to change host |
18+
| `--no-open` | `false` | Do not open the browser automatically |
19+
| `--basic-auth` | _(none)_ | Enable HTTP Basic Auth: `user:pass` |
20+
21+
## Quick start
22+
23+
```bash
24+
# Run an audit and save the report
25+
hardbox audit --format json --output /var/log/hardbox/reports/$(date +%s).json
26+
27+
# Start the dashboard
28+
hardbox serve --reports-dir /var/log/hardbox/reports/
29+
# → hardbox dashboard → http://127.0.0.1:8080
30+
```
31+
32+
## Dashboard routes
33+
34+
| Route | Description |
35+
|---|---|
36+
| `/` | Report list — all JSON reports sorted by date |
37+
| `/report/<session_id>` | Single report — findings table per module |
38+
| `/diff/<before_id>/<after_id>` | Inline diff between two reports |
39+
| `/api/reports` | JSON API — report metadata list |
40+
41+
## Security
42+
43+
- Binds to `127.0.0.1` by default — not reachable from the network.
44+
- To expose on the local network (e.g. in a shared lab), set `--addr 0.0.0.0:8080` and protect with `--basic-auth`.
45+
- Read-only — no write operations are exposed via HTTP.
46+
- All assets are embedded in the binary via `go:embed` — no external CDN or internet access required.
47+
48+
## Examples
49+
50+
```bash
51+
# Custom port, no browser
52+
hardbox serve --port 9000 --reports-dir ./reports/ --no-open
53+
54+
# Shared access with basic auth
55+
hardbox serve --addr 0.0.0.0:8080 --basic-auth ops:s3cr3t --reports-dir /var/log/hardbox/reports/
56+
57+
# Compare two specific reports from the UI
58+
# Navigate to: http://127.0.0.1:8080/diff/<before_session_id>/<after_session_id>
59+
```

internal/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func newRootCmd(version string) *cobra.Command {
4747
newDiffCmd(),
4848
newFleetCmd(gf),
4949
newPluginCmd(gf),
50+
newServeCmd(),
5051
)
5152

5253
return root

internal/cli/serve.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os/exec"
7+
"runtime"
8+
"strings"
9+
10+
"github.com/rs/zerolog/log"
11+
"github.com/spf13/cobra"
12+
13+
"github.com/hardbox-io/hardbox/internal/serve"
14+
)
15+
16+
func newServeCmd() *cobra.Command {
17+
var (
18+
port int
19+
addr string
20+
reportsDir string
21+
noOpen bool
22+
basicAuth string
23+
)
24+
25+
cmd := &cobra.Command{
26+
Use: "serve",
27+
Short: "Start a local web dashboard to browse audit reports",
28+
Long: `serve starts a read-only HTTP dashboard on localhost that lets you
29+
browse audit reports, inspect findings, and compare any two reports side by side.
30+
31+
The server binds to 127.0.0.1 by default and never exposes to the network
32+
unless --addr is set explicitly.
33+
34+
Examples:
35+
# Start on default port 8080
36+
hardbox serve --reports-dir /var/log/hardbox/reports/
37+
38+
# Custom port
39+
hardbox serve --port 9000 --reports-dir ./reports/
40+
41+
# Bind to custom address with basic auth
42+
hardbox serve --addr 0.0.0.0:8080 --basic-auth admin:secret
43+
44+
# Don't open browser automatically
45+
hardbox serve --no-open --reports-dir ./reports/`,
46+
RunE: func(cmd *cobra.Command, args []string) error {
47+
listenAddr := resolveAddr(addr, port)
48+
49+
cfg := serve.Config{
50+
Addr: listenAddr,
51+
ReportsDir: reportsDir,
52+
BasicAuth: basicAuth,
53+
}
54+
55+
srv, err := serve.New(cfg)
56+
if err != nil {
57+
return fmt.Errorf("creating server: %w", err)
58+
}
59+
60+
url := "http://" + srv.Addr()
61+
fmt.Fprintf(cmd.OutOrStdout(), "hardbox dashboard → %s\n", url)
62+
fmt.Fprintf(cmd.OutOrStdout(), "reports dir → %s\n", reportsDir)
63+
fmt.Fprintf(cmd.OutOrStdout(), "press Ctrl+C to stop\n\n")
64+
65+
if !noOpen {
66+
go openBrowser(url)
67+
}
68+
69+
log.Debug().Str("addr", listenAddr).Str("reports_dir", reportsDir).Msg("serve: starting")
70+
return srv.Start(cmd.Context())
71+
},
72+
}
73+
74+
cmd.Flags().IntVar(&port, "port", 8080, "port to listen on (ignored when --addr is set)")
75+
cmd.Flags().StringVar(&addr, "addr", "", "full listen address, e.g. 127.0.0.1:8080 (overrides --port)")
76+
cmd.Flags().StringVar(&reportsDir, "reports-dir", ".", "directory containing JSON audit reports")
77+
cmd.Flags().BoolVar(&noOpen, "no-open", false, "do not open the browser automatically")
78+
cmd.Flags().StringVar(&basicAuth, "basic-auth", "", "enable HTTP basic auth, format: user:pass")
79+
80+
return cmd
81+
}
82+
83+
// resolveAddr returns the listen address, preferring --addr over --port.
84+
func resolveAddr(addr string, port int) string {
85+
if addr != "" {
86+
// Ensure it has a host component — default to 127.0.0.1 for safety.
87+
if !strings.Contains(addr, ":") {
88+
return net.JoinHostPort("127.0.0.1", addr)
89+
}
90+
return addr
91+
}
92+
return net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port))
93+
}
94+
95+
// openBrowser opens url in the user's default browser.
96+
func openBrowser(url string) {
97+
var cmd *exec.Cmd
98+
switch runtime.GOOS {
99+
case "darwin":
100+
cmd = exec.Command("open", url)
101+
case "windows":
102+
cmd = exec.Command("cmd", "/c", "start", url)
103+
default:
104+
cmd = exec.Command("xdg-open", url)
105+
}
106+
if err := cmd.Start(); err != nil {
107+
log.Debug().Err(err).Msg("serve: could not open browser")
108+
}
109+
}
110+

0 commit comments

Comments
 (0)