backend/app.py creates the Flask app and registers blueprints from backend/handlers.
Each handler imports controller functions, e.g., backend/controllers/rotator.py calls crud/rotators.py.
CRUD functions perform SQLAlchemy queries against models defined in backend/db/models.py.
Decoding workers are launched as separate processes; they read raw IQ samples from backend/audio/audiobroadcaster.py and write results to the database via crud/transmitters.py.
The React frontend starts at frontend/index.html, mounts App.jsx, and talks to the API using frontend/src/api/*.js.
The wiring is linear: API → handlers → controllers → crud → models. No deep recursion or cyclic dependencies were detected in the call graph.
How To Use It
Setup
# clone the original project
git clone https://github.com/moses-y/ground-station
cd ground-station
# build the Docker image (the Dockerfile is at the repo root)
docker build -t ground-station:dev .
Configuration
Backend environment: backend/.env (not in the repo; create one).
Frontend env: frontend/.env.development or frontend/.env.production – contains API base URL and feature flags.
Database: backend/pyproject.toml lists psycopg2-binary; set DATABASE_URL in the backend env.
Running
# start the container (exposes http://localhost:5000 for API, http://localhost:5173 for UI)
docker compose up --build
The Flask app runs from backend/app.py; Vite serves the React frontend from frontend/.
Real‑World Use
A hobbyist at an amateur satellite ground station runs the container on a Raspberry Pi.
# schedule a pass
curl -X POST http://localhost:5000/api/observations/ \
-H "Content-Type: application/json" \
-d '{"satellite_id": 25544, "time": "2026-10-12T00:00:00Z"}'
The UI shows the pass on the map, streams the SDR waterfall, and stores decoded packets in backend/db/models.py tables for later review.
Ground‑Station delivers a full‑stack solution for satellite tracking and SDR decoding. Its clear separation of concerns (Flask API, React UI, SQLAlchemy models, decoders) makes extension straightforward. The main caveat is the committed environment files; review or remove them before production use. Suitable for operators who need a consolidated, web‑based ground‑station stack.
What the analyser found
Deployment readiness
6/7
✓Container image
✓CI pipeline
✓Lockfile committed
✓Test suite
✓README
✓License
✗No committed secrets
Composition
2,543 files
Python460
JSX352
JavaScript121
JSON102
Markdown26
YAML8
ReactDjangoFlaskExpressDocker
Module dependencies
The 10 most depended-upon modules of 888, from static import analysis. Red outline marks a module in an import cycle.
Ranked by severity × confidence × production reach. Reach is the honest discriminator across a collection that is mostly other people's code: the same finding matters more in something that ships.
high3
medium7
low1
Pin third-party GitHub Actions to a commit SHAhigh5 occurrences
A tag can be moved, so the action running with your token and secrets is whatever its owner last pushed; this is how tj-actions/changed-files leaked secrets from thousands of repos.
Fix: Replace each @vN with the 40-character commit SHA, keep # vN as a comment, and let Dependabot bump the SHAs.
Commit a lockfile beside the manifesthigh
backend/pyproject.toml
manifest with no lockfile in scope
An unlocked range means the artifact you tested and the artifact you ship can contain different transitive code, so a malicious patch release reaches production with no diff.
Fix: Run the package manager once and commit the generated lockfile.
Remove the committed .env and rotate what it holdshigh2 occurrences
A tracked .env is the most common route for a working key to reach a public clone, and it is the file the app actually loads, so the value is usually live.
Fix: git rm --cached the file, add it to .gitignore, rotate every credential it names, commit a .env.example with empty values.
Declare least-privilege permissions for GITHUB_TOKENmedium
.github/workflows/tests.yml
1 workflow(s) declare no permissions
With no declaration the token inherits the repository default, so any injected step can push commits or mint releases from inside your own CI.
Fix: Add permissions: contents: read at the top of the workflow and widen per job only where needed.
Enable Dependabot or Renovatemedium
4 manifest(s), no update bot configured
Without a bot a published advisory sits unpatched until someone audits by hand, which across 1,322 repositories means never.
Fix: Commit .github/dependabot.yml covering the repo ecosystems plus github-actions.
Pin the container base image by digestmedium2 occurrences
Dockerfile
node:22-bookworm-slim, ubuntu:noble-20250925
An untagged or mutable base means today's build and last month's contain different libc and a different CVE set, with no record of which shipped.
Fix: Use image:tag@sha256:<digest> and enable Dependabot's docker ecosystem.
Gate pull requests on a dependency vulnerability scanmedium
.github/workflows
no dependency scan in CI
This is the one gate that would catch a known-vulnerable package before it reaches a build, and no repository in the sample had it.
Fix: Add dependency-review-action on pull_request, or osv-scanner on push and a schedule.
Move large binaries to Git LFS or out of the repositorymedium4 occurrences
backend/images/satellites/full/60476.png
4 blobs over 5MB: 60476.png 8.0MB, 56210.png 6.2MB, 60532.png 5.6MB
One repository here carries twenty blobs over 5MB including a 10.9MB spreadsheet, so every clone and every CI checkout pays for data nobody diffs.
Fix: Track those extensions with LFS, or move datasets to object storage and fetch them in a setup step.
Set persist-credentials: false on checkoutmedium
.github/workflows/tests.yml
checkout keeps the token, then dependencies are installed
The token stays in .git/config for every later step, so a malicious postinstall script reads a pushable credential without one ever being passed to it.
Fix: Add with: persist-credentials: false, and pass an explicit token only to the step that pushes.
Add a non-root USER to the imagemedium
Dockerfile
CMD or ENTRYPOINT with no USER directive
A process running as root in the container is root against every mounted volume, and it turns any container escape or writable-mount mistake from a contained problem into a host one.
Fix: Create an unprivileged user, chown what it needs, and end the Dockerfile with USER.
Set timeout-minutes on the workflow jobslow2 occurrences
.github/workflows/cla.yml
2 workflow(s) declare no job timeout
A wedged step runs to the six-hour platform default, which on a two-hourly schedule means three runs overlap behind it.
Fix: Add timeout-minutes with a realistic bound to each job.
Checked deterministically against the repository tree and a bounded set of its files: committed credentials, unpinned actions and base images, missing lockfiles and update bots, workflows that discard failures, published advisories against the declared dependencies, runtime configuration, licensing and notebook reproducibility. No language model is involved in this section.