Skip to content

Tags: ulm0/argus

Tags

v0.6.1

Toggle v0.6.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix/codebase audit (#30)

* fix: resolve bugs, races, and perf issues across backend and web

Codebase-wide audit fixing ~120 verified issues.

Data-loss / corruption:
- cleanup: reject malformed execute body instead of defaulting to a
  real (non-dry-run) deletion
- quick_edit: require a clean RW unmount before restoring the gadget
  LUN, so the car and local kernel never write one FAT image at once;
  wait out the callback on timeout; refresh the lock mtime while busy
- mount: kill only foreign mount holders on unmount retry, never the
  argus daemon itself (was `fuser -km`)
- loop: drop LO_FLAGS_AUTOCLEAR so the device can't self-detach before
  callers use it
- fsck: refuse to check an image that is live (present mode or an open
  quick-edit window)
- chime: reject writes when the partition is unmounted instead of
  writing to CWD and reporting success

Correctness:
- api: share one chime.Service so schedule/group edits take effect and
  are not clobbered by the running scheduler
- telegram: run watcher/queue regardless of boot-time enabled so
  runtime enable works; guard config fields and queue persistence
- telegram/watcher: retry event video lookup instead of dropping events
  whose clips are not yet written
- config: validate the whole PATCH before mutating live config; store
  generated config 0600 (holds secrets)
- web: fix folder-drop >100 file loss, schedule loading, upload error
  handling, SSE reconnect, fetch races, and audio/interval leaks

Security:
- login brute-force backoff, Sec-Fetch-Site CSRF guard, X-Real-IP/XFF
  validation, and no default-cred disclosure to anonymous callers

Perf:
- stream uploads, skip gzip on binary media, bound SEI parser
  allocations, and stop hidden SystemPanel polling on phones

Also removes dead internal/system/fsck package.

* chore(web): bump postcss and @babel/core past Dependabot alerts

next 16.2.6 pulls postcss 8.4.31 (GHSA-qx2v-qp2m-jg93) and
@babel/core 7.29.0 (GHSA-4x5r-pxfx-6jf8) transitively. Pin patched
versions via pnpm overrides (workspace file — pnpm 11 no longer reads
package.json pnpm.overrides).

* fix(security): close path-traversal gaps flagged by CodeQL

Adversarial audit of the 37 go/path-injection alerts: 28 are false
positives (isWithinDir/withinBase after filepath.Clean, which CodeQL's
query does not recognize), 9 are real defense-in-depth gaps, fixed here.

- music: validate temp path (destPath+".tmp"/".assembling") and the
  MoveFile destination dir; a filename collapsing destPath to the music
  root let the atomic-write temp file / mkdir land in a sibling dir
  outside the root
- video: reject ".." event names in GetEventDetails before they reach
  the parseEvent read chain
- video handlers: verify Thumbnail/SessionThumbnail thumbPath stays
  within ThumbnailDir before stat/generate/serve

v0.6.0

Toggle v0.6.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix: resolve bugs, races, and perf issues across backend and web (#29)

Codebase-wide audit fixing ~120 verified issues.

Data-loss / corruption:
- cleanup: reject malformed execute body instead of defaulting to a
  real (non-dry-run) deletion
- quick_edit: require a clean RW unmount before restoring the gadget
  LUN, so the car and local kernel never write one FAT image at once;
  wait out the callback on timeout; refresh the lock mtime while busy
- mount: kill only foreign mount holders on unmount retry, never the
  argus daemon itself (was `fuser -km`)
- loop: drop LO_FLAGS_AUTOCLEAR so the device can't self-detach before
  callers use it
- fsck: refuse to check an image that is live (present mode or an open
  quick-edit window)
- chime: reject writes when the partition is unmounted instead of
  writing to CWD and reporting success

Correctness:
- api: share one chime.Service so schedule/group edits take effect and
  are not clobbered by the running scheduler
- telegram: run watcher/queue regardless of boot-time enabled so
  runtime enable works; guard config fields and queue persistence
- telegram/watcher: retry event video lookup instead of dropping events
  whose clips are not yet written
- config: validate the whole PATCH before mutating live config; store
  generated config 0600 (holds secrets)
- web: fix folder-drop >100 file loss, schedule loading, upload error
  handling, SSE reconnect, fetch races, and audio/interval leaks

Security:
- login brute-force backoff, Sec-Fetch-Site CSRF guard, X-Real-IP/XFF
  validation, and no default-cred disclosure to anonymous callers

Perf:
- stream uploads, skip gzip on binary media, bound SEI parser
  allocations, and stop hidden SystemPanel polling on phones

Also removes dead internal/system/fsck package.

v0.5.0

Toggle v0.5.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Security/bug/perf fixes + web UI login (#27)

* fix: patch path traversal, AP config injection, token leak, and update-check bugs

Security and correctness fixes found during a code review of the API,
service, and system layers:

- music: HandleChunk performed no path-traversal validation while reading
  filename/path from raw form values (not sanitized by the stdlib like
  multipart filenames are), allowing an arbitrary file write. Validate the
  upload ID, sanitize the filename to its base, and constrain the target and
  destination to the music directory. Add the same containment backstop to
  SaveFile.
- chime: UploadChime wrote the upload before any containment check, unlike its
  sibling methods. Add the missing path-traversal guard.
- ap: SSID/passphrase/DHCP/CIDR values were templated into hostapd.conf and
  dnsmasq.conf without escaping, so an embedded newline could inject directives
  (e.g. wpa=0 to disable AP encryption). Validate inputs in UpdateAPConfig and
  refuse to emit a config containing control characters as a backstop for the
  PATCH /api/config path.
- telegram: network errors are *url.Error and embed the request URL, which
  contains the bot token; redact the token before returning/logging it.
- config: check_on_startup was a plain bool force-set to true whenever false,
  so an explicit `check_on_startup: false` could never disable startup update
  checks. Make it a *bool (like samba_enabled) with a CheckUpdateOnStartup()
  accessor that defaults to true only when unset.
- logs: validate the systemd unit name against the unit-name charset.
- video: cap thumbnail width/height to bound ffmpeg work.

Adds regression tests for the music traversal fix and the update-check default.

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* fix(web,build): MP4 parser DoS, URL encoding, resource leaks, and build fragility

Frontend and packaging fixes from an audit of the web UI, installer, CI, and
build config (the Go API XSS surface was reviewed and found clean — the only
dangerouslySetInnerHTML is a static literal).

Frontend:
- sei-parser: findBox could infinite-loop on a malformed MP4 whose 64-bit
  extended box size is 0 (or smaller than its own header) — `pos += size`
  never advanced, hanging the synchronous main-thread parse on attacker-
  supplied USB bytes and freezing the tab. Guard with `if (size < headerSize)
  break;`.
- api: streamURL/downloadURL/seiURL/telemetryURL/playURL/deleteFile/deleteDir
  interpolated server-controlled path segments (video/music filenames from the
  Tesla USB) into request URLs without encoding, unlike every other builder.
  A name containing #/?/%/&/= could truncate the path, inject a query string,
  or (for the delete endpoints) target the wrong resource. Add an encodePath()
  helper that percent-encodes each segment while preserving separators.
- Resource leaks: revoke loaded-MP4 blob URLs on unmount (videos/local),
  close the previous AudioContext before opening a new one (AudioTrimmer),
  and pause audio playback on unmount (music page).

Build/packaging:
- install-argus.sh: reject unsupported architectures instead of silently
  installing the armv6 binary, and verify the downloaded binary against the
  release's published sha256 checksum before installing it as root.
- Makefile: use pnpm (the repo ships only pnpm-lock.yaml; goreleaser uses
  pnpm) and stage the static export into cmd/argus/web/out where the binary
  embeds it, so `make all` actually works.
- Add cmd/argus/web/out/.gitkeep so `//go:embed all:web/out` compiles on a
  clean checkout before the frontend is built.

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* fix(api,services): close arbitrary-delete, OOM, panic, and concurrency bugs

Backend fixes from an exhaustive adversarially-verified audit of internal/ and
cmd/ (19 confirmed findings; the genuinely serious ones first).

Security / DoS:
- cleanup: cleanup folder-policy keys were joined onto the TeslaCam path and fed
  to os.RemoveAll with no validation, so an unauthenticated client could POST a
  key like "../../etc" and have the root process recursively delete arbitrary
  directories. Reject non-single-segment keys in SavePolicies and skip unsafe
  keys in CalculateCleanupPlan (also protects the boot-time cleanup path).
- router: the request-body size limit (and gzip skip) was bypassable by sending
  Accept: text/event-stream on any upload POST, removing the only cap before
  handlers io.ReadAll the body into RAM. Gate the SSE exemption on the actual
  /api/events/stream and /api/logs routes instead of the client header.
- chime: ValidateTeslaWAV allocated make([]byte, chunkSize) from an untrusted
  uint32, so a tiny crafted WAV could force a ~4 GiB allocation. Reject a chunk
  larger than the file.
- video: bound the mvhd box payload in scanBoxes (same unbounded-alloc class).
- telemetry: a crafted protobuf length-delimited field could drive the parse
  position negative and panic on data[pos]; bound the skip in 64-bit space.
- lightshow: UploadZip extracted with no per-entry/total cap (zip bomb / disk
  fill); add per-entry, total, and entry-count limits via io.CopyN.

Concurrency / correctness:
- chime: GetSchedule returned an interior pointer into the mutex-protected slice;
  return a copy so callers can't race the per-minute tick / reslices.
- fsck: Cancel cleared the running flag while the worker was alive, letting a
  racing Start spawn a second worker; only the worker clears running now.
- config: serialize Save() so concurrent savers can't clobber the shared .tmp
  file. (The broader cfg field-write-vs-marshal race is documented for a
  follow-up that routes all writers through one lock.)
- updater: restore the backup binary if the swap rename fails, so a failed
  update can't leave the service with no executable.

Hardening:
- config PATCH: validate offline_ap SSID/passphrase/CIDR/IP/ping_target like
  UpdateAPConfig does, instead of writing them through unchecked.
- ap: tighten interface-name validation (no '/', no leading '-').
- wifi: reject a ping target that ping would parse as a flag.
- webhook/telegram: drain response bodies before Close for keep-alive reuse.
- realip: handle a leading-comma X-Forwarded-For without blanking RemoteAddr.

Adds regression tests for the cleanup traversal, SEI panic, and WAV allocation.

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* fix(security): add explicit path-confinement barriers at delete/extract/upload sinks

Resolve a CodeQL high-severity path-injection finding and harden the adjacent
sinks. The existing guards used filepath.Base equality / ContainsAny checks,
which confine the path in practice but are not recognized as sanitizers by
static analysis, leaving the os.RemoveAll / os.Create sinks flagged.

Add the canonical HasPrefix(joined, base+separator) containment check (the same
withinBase pattern already used in the video handler) at each sink:
- cleanup: confine the resolved folder path under the TeslaCam root before it is
  used to build the event paths ExecuteCleanup removes.
- lightshow: confine the ZIP extraction target under the show directory.
- music: confine the chunk-upload scratch directory (derived from upload_id)
  under the music directory.

No behavior change for valid inputs; these make the traversal-safety invariant
explicit and analyzable.

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* harden(server): set ReadHeaderTimeout and MaxHeaderBytes

The HTTP server had no read timeout, so a slow-header (slowloris) client could
hold a connection open indefinitely on the single-core Pi. Bound header reads
and header size. Write/Idle timeouts are intentionally left unset so they don't
truncate the long-lived SSE streams (/api/logs, /api/events/stream).

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* feat(auth): add login with a default credential

Gate the web UI/API behind a session login, since the device's AP exposes the
API (which runs as root) to anyone in radio range, and the no-auth API was also
open to DNS-rebinding/CSRF from a site the operator visits while connected.

Backend:
- config: add web.auth_enabled (*bool, default true), auth_username, and
  auth_password, shipping with default credentials admin / argus. AuthEnabled()
  and UsingDefaultAuth() accessors.
- auth: stateless HMAC-signed session tokens (internal/auth), signed with the
  previously-unused web.secret_key, with a 7-day TTL and constant-time checks.
- handlers: POST /api/login, POST /api/logout, GET /api/auth/status. Login
  compares credentials in constant time and sets an HttpOnly, SameSite=Strict
  session cookie (the SameSite attribute also blunts CSRF).
- middleware: RequireAuth gates every /api/ route except the auth endpoints;
  non-API routes (static UI shell, captive-portal probes) stay open so the
  login page can load. Returns 401 JSON when unauthenticated.
- config GET/PATCH: expose/edit auth settings (password is write-only and never
  returned); credentials are changeable from the settings API.

Frontend:
- AuthGate wraps the app: checks /api/auth/status on mount and renders a login
  form when auth is enabled and the user isn't authenticated.
- the API client dispatches a global event on any 401 so an expired session
  re-prompts for login without each caller handling it.
- SystemPanel gains a Sign out button.

Note: the default credentials are intentionally weak and meant to be changed;
the login screen and settings surface a "using default credentials" nudge.

Tests cover session sign/verify/expiry/tamper and the login + gating flow.

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* fix(auth): set cookie Secure attribute when the request is over HTTPS

CodeQL flagged the session cookies for not setting Secure. Argus serves plain
HTTP on the local network/AP by default, so forcing Secure unconditionally would
stop the browser from ever sending the cookie and break login. Instead set
Secure when the request actually arrives over TLS — directly (r.TLS) or via a
TLS-terminating reverse proxy (X-Forwarded-Proto: https) — which protects
proxied deployments without breaking the default HTTP one. Spoofing the header
only makes the cookie more restrictive, so it can't weaken the session.

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* docs(readme): document the web UI login and default credentials

Add an "Accessing the web UI" section (default admin / argus, how to change or
disable, session-cookie details), a web-interface feature bullet, and a `web`
row in the config sections table.

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

* feat(security): DNS-rebinding Host check + default-credential startup warning

Per review: keep the default login (admin/argus) but make the device advise
changing it, and add the DNS-rebinding defense discussed on the auth work.

- middleware: RequireLocalHost rejects /api/ requests whose Host header isn't a
  local identity (IP literal, localhost, bare single-label name, *.local/*.lan)
  or an operator-configured host. Blocks a malicious public site that rebinds
  its domain to the device's LAN IP from driving the API in a victim's browser.
  Root paths (captive-portal probes, hit with public Host headers on purpose)
  and static assets are not gated.
- config: web.allowed_hosts lets reverse-proxy/custom-domain deployments add
  their FQDN to the allowlist.
- run: log a warning at startup when the default credentials are still in use,
  and when auth is disabled entirely, so headless operators are nudged.
- docs: document allowed_hosts and the rebinding defense in the README.

Tests cover the Host allow/deny matrix (local identities pass, public domains
are blocked, non-API paths are exempt).

Claude-Session: https://claude.ai/code/session_016vMstjHpKgvddCfBPTSrRH

---------

Co-authored-by: Claude <noreply@anthropic.com>

v0.4.1

Toggle v0.4.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
WiFi reconnect reliability + Bluetooth power/scan/pairing controls (#26)

v0.4.0

Toggle v0.4.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Security/review fixes, AP reliability, WiFi saved-network management,…

… Tesla encrypted-clip UX (#25)

v0.3.5

Toggle v0.3.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(ap): evict stale dnsmasq/hostapd before restarting AP (#20)

After a crash or unclean shutdown the previous dnsmasq instance keeps
holding the 192.168.4.1:53 socket, causing "Address already in use" on
the next startAPLocked call. Kill orphaned processes (same pattern used
by stopAPLocked) before touching the interface so the new dnsmasq can
bind successfully.

https://claude.ai/code/session_0168SwBt9HqT1qU9VirQD9Js

Co-authored-by: Claude <noreply@anthropic.com>

v0.3.4

Toggle v0.3.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(release): migrate pnpm build approvals to pnpm-workspace.yaml (#19)

pnpm 11 dropped support for the pnpm field in package.json.
Move sharp and unrs-resolver build approvals to allowBuilds in
pnpm-workspace.yaml per the pnpm 11 migration guide.

https://claude.ai/code/session_01EJxAoYdTJTqdidy1oZeJdz

Co-authored-by: Claude <noreply@anthropic.com>

v0.3.3

Toggle v0.3.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Claude/single video download (#14)

* revert: restore Inter font, remove Universal Sans setup

https://claude.ai/code/session_01YL6zD5ZwvrzNQLXm8EggEM

* fix: composed view default, HUD in composed mode, proper pedal icons

- DashcamPlayer opens in composed view by default.
- ArgusHUD is now rendered over the master (front) camera cell in
  composed mode; HUD toggle button is always visible regardless of mode.
- Pedal icons replaced with solid T-shaped silhouettes matching Tesla's
  official dashcam viewer: brake = wide plate + stem, throttle =
  narrower plate + stem.

https://claude.ai/code/session_01YL6zD5ZwvrzNQLXm8EggEM

* feat: GPS map overlay using SEI telemetry data

Adds an interactive map overlay to the dashcam player using Leaflet with
CartoDB dark tiles. Shows the driven route as a polyline and a heading-aware
position marker that follows the vehicle in real-time as the video plays.

- MapOverlay.tsx: client-side Leaflet map, dynamically imported (ssr:false)
  to avoid server-side window access errors
- Map toggle button (map pin icon) in the player controls bar
- mapEnabled persisted in localStorage alongside hudEnabled
- SEI telemetry is shared between HUD and map (loaded once, used by both)
- Overlay positioned bottom-right of video area in both single and composed modes
- GPS frames with lat/lng = 0,0 filtered out (protobuf default = no fix)

* feat: enable map overlay by default

* feat: resizable map/HUD, map theme setting, light tiles by default

* feat: realistic pedal icons (outline style, brake wide pad, gas angled arm)

* fix: add missing react-icons dependency

* feat: single video download button in player controls and per-camera cells

---------

Co-authored-by: Claude <noreply@anthropic.com>

v0.3.2

Toggle v0.3.2's commit message

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
chore: note

v0.3.1

Toggle v0.3.1's commit message

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
chor: update ns