Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,52 @@ permissions:
contents: read

jobs:
investigator:
name: investigator build and tests
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: Rust toolchain
shell: bash
run: rustup toolchain install 1.93.0 --profile minimal --component clippy,rustfmt
- name: Rust checks
shell: bash
run: |
set -euo pipefail
cargo +1.93.0 fmt --manifest-path investigate/Cargo.toml --check
cargo +1.93.0 clippy --locked --manifest-path investigate/Cargo.toml --all-targets -- -D warnings
cargo +1.93.0 test --locked --manifest-path investigate/Cargo.toml
cargo +1.93.0 build --locked --manifest-path investigate/Cargo.toml
bash -n investigate/install-and-run.sh
shellcheck investigate/install-and-run.sh
investigate/install-and-run.sh --help

- name: investigator static release build
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y musl-tools
rustup target add --toolchain 1.93.0 x86_64-unknown-linux-musl
export CC_x86_64_unknown_linux_musl=musl-gcc
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc
cargo +1.93.0 build --locked --release --target x86_64-unknown-linux-musl --manifest-path investigate/Cargo.toml

- name: investigator case smoke test
shell: bash
run: |
set -euo pipefail
printf '%s\n' 'POST https://example.test/upload' > /tmp/opsforge-investigator-fixture.log
sudo investigate/target/debug/opsforge-investigate \
--non-interactive --no-exfil --no-timeline --no-downloads \
--no-inventory --no-capture \
--import /tmp/opsforge-investigator-fixture.log \
--output /var/lib/opsforge-investigator-ci
page="$(sudo find /var/lib/opsforge-investigator-ci -path '*/dashboard/index.html' -print -quit)"
test -n "$page"
sudo ./bin/validate-output-contract "$(dirname "$(dirname "$page")")"

shell:
name: shell validation
runs-on: ubuntu-latest
Expand Down
26 changes: 24 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ jobs:
with:
fetch-depth: 0

- name: build Linux investigator bundle
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y musl-tools
rustup toolchain install 1.93.0 --profile minimal
rustup target add --toolchain 1.93.0 x86_64-unknown-linux-musl
cargo +1.93.0 test --locked --manifest-path investigate/Cargo.toml
export CC_x86_64_unknown_linux_musl=musl-gcc
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc
cargo +1.93.0 build --locked --release --target x86_64-unknown-linux-musl --manifest-path investigate/Cargo.toml
cp investigate/target/x86_64-unknown-linux-musl/release/opsforge-investigate .
tar -czf opsforge-investigate-linux-x86_64.tar.gz opsforge-investigate
sha256sum opsforge-investigate-linux-x86_64.tar.gz > opsforge-investigate-linux-x86_64.tar.gz.sha256

- name: build release notes
shell: bash
run: |
Expand Down Expand Up @@ -98,10 +114,16 @@ jobs:

tag="${GITHUB_REF_NAME}"
if gh release view "$tag" >/dev/null 2>&1; then
echo "release already exists: $tag"
for asset in opsforge-investigate-linux-x86_64.tar.gz opsforge-investigate-linux-x86_64.tar.gz.sha256; do
if ! gh release view "$tag" --json assets --jq '.assets[].name' | grep -Fx "$asset" >/dev/null; then
gh release upload "$tag" "$asset"
fi
done
exit 0
fi

gh release create "$tag" \
--title "$tag" \
--notes-file release-notes.md
--notes-file release-notes.md \
opsforge-investigate-linux-x86_64.tar.gz \
opsforge-investigate-linux-x86_64.tar.gz.sha256
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v0.6.0

- Added a Linux investigator with guided setup, source coverage, and a shareable
offline case dashboard.
- Added sequential raw and JSONL collection for retained logs, browser history,
Chromium downloads, active sockets, imports, file inventory, and timed packets.
- Added case hashes and a verified release bootstrap for Linux x86_64.
- Added Rust checks and a case smoke test to Linux CI.

## v0.5.0 - 2026-05-20

- Reformatted source files for readability and reviewability.
Expand Down
65 changes: 65 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Investigator interface

## 1. Purpose

The terminal interface configures and follows a case run. Offline HTML pages
make its evidence and limits readable to people who did not operate the tool.
The case directory is the source of truth; neither view hides failed sources.

## 2. Audience and tasks

- An operator chooses the output, collection categories, extra logs, and capture duration.
- An investigator checks findings, the timeline, and the raw source behind each item.
- A reviewer opens the offline report without a running server.

## 3. Visual direction

Use a restrained operations surface: dark ink, slate panels, cyan for active
work, amber for gaps, and red for failed collection. The case status is the
first visual anchor. Color never carries status alone; every state has text.

## 4. Tokens

| Role | Value |
| --- | --- |
| page | `#101923` |
| panel | `#172633` |
| panel raised | `#1d3141` |
| border | `#355061` |
| text | `#e9f1f5` |
| muted | `#a7bcc8` |
| active | `#59d5d0` |
| warning | `#f1bd68` |
| failure | `#f07878` |
| type | system sans for prose, system monospace for evidence |
| spacing | multiples of 4 px |

## 5. Primitives and states

- TUI section row: idle, focused, selected, unavailable.
- TUI setting row: label, current value, short help.
- TUI run row: pending, running, collected, empty, failed, skipped.
- Web status badge: same named states, always with text.
- Web evidence table: sortable by time in source order, readable without scripts.
- Web source link: relative path back to raw or normalized evidence.

## 6. Layout and navigation

The TUI has a guided configuration screen, a review action, and a run view.
Keyboard navigation uses arrows, Space, Enter, and Escape; a narrow terminal
keeps one column. The dashboard has a shared header and links for Overview,
Exfiltration, Timeline, Downloads, and Coverage. Pages scroll normally and
tables can scroll horizontally on small screens.

## 7. Accessibility and safety

Use semantic HTML headings, tables, nav, and visible focus states. Keep the
dashboard useful without JavaScript or network access. Escape all collected
text before it reaches HTML. Distinguish observed evidence from inference and
show unparsed or unavailable sources near findings.

## 8. Accepted limits

No endpoint run can reconstruct events that were never logged. A displayed
historical range means the range of available evidence, not the lifetime of an
application. Imported logs retain their own provenance and time zone context.
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# opsforge

opsforge is a shell-only toolkit for SOC, NOC, Linux, Windows, IR, and ops work.
opsforge is a defensive toolkit for SOC, NOC, Linux, Windows, IR, and ops work.

It is built for the boring stuff that actually matters: triage, persistence checks, disk issues, timelines, network exposure, and reports you can use.

Expand All @@ -11,12 +11,12 @@ No fake polish. No random helper stack. No toy recon wrappers.
opsforge is beta. The scripts are useful, but they are still being tested across
real systems and CI runners.

The project stays shell-only:
The existing command toolkit stays shell-only:

- Linux and Unix scripts use Bash or POSIX sh where that makes sense.
- Windows scripts use PowerShell.
- Core tooling does not use Python, Go, Rust, Node.js, Ruby, Perl, or compiled
helpers.
- The Linux investigator is a separate prebuilt Rust runner for guided case
collection and offline reporting. It does not require Rust on the target.
- Scripts are read-only by default unless a script clearly says otherwise and
exposes an explicit action flag.

Expand Down Expand Up @@ -57,6 +57,29 @@ Windows:

## Commands

After the next tagged release, launch the Linux investigator with one command:

```bash
curl -fsSL https://raw.githubusercontent.com/iamb4uc/opsforge/main/investigate/install-and-run.sh -o /tmp/opsforge-investigate-bootstrap && bash /tmp/opsforge-investigate-bootstrap --install-deps --remove-bootstrap
```

The bootstrap verifies the release archive with SHA-256 and, with `--install-deps`, installs missing
`tcpdump`, `ss`, or `timeout` through the host package manager, and opens the
setup TUI. It checks existing root access first, then requests `sudo` if needed.
The default case parent is `/var/lib/opsforge/cases`; the TUI lets you change it
to another root-owned location whose parent directories are not group or world writable.
The live capture defaults to `5m` and accepts positive `s`, `m`, `h`, or `d`
durations. The temporary binary and bootstrap are removed after the run; the
case directory stays. Use `--output PATH`, `--duration 1h`, or `--import PATH`
after `--remove-bootstrap` when needed.

The case contains raw evidence, normalized JSONL, source coverage, hashes,
findings, a Markdown report, and offline HTML pages. Browser history and log
keywords are leads, not proof of upload. Historical application traffic can
only be attributed where the device or imported logs retained that detail.
See [Linux investigator notes](docs/linux-investigator.md) for source coverage
and limits.

Install on Linux/Unix:

```bash
Expand Down
4 changes: 3 additions & 1 deletion bin/test
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,12 @@ test_forbidden_files() {
-path "$ROOT/.git" -prune -o \
-path "$ROOT/.ci-artifacts" -prune -o \
-path "$ROOT/output" -prune -o \
-path "$ROOT/investigate/target" -prune -o \
-type f \( \
-name '*.py' -o -name '*.go' -o -name '*.rs' -o -name '*.js' -o \
-name '*.ts' -o -name '*.rb' -o -name '*.pl' -o -name '*.exe' -o \
-name '*.dll' -o -name '*.so' -o -name '*.dylib' \
\) -print)"
\) ! \( -path "$ROOT/investigate/src/*.rs" -o -path "$ROOT/investigate/tests/*.rs" \) -print)"
[ -z "$files" ] || fail "forbidden core language or binary files found: $files"
}

Expand All @@ -247,6 +248,7 @@ test_readability() {
-path "$ROOT/.git" -prune -o \
-path "$ROOT/.ci-artifacts" -prune -o \
-path "$ROOT/output" -prune -o \
-path "$ROOT/investigate/target" -prune -o \
-path "$ROOT/examples" -prune -o \
-type f \( \
-name '*.md' -o -name '*.sh' -o -name '*.ps1' -o -name '*.yml' -o \
Expand Down
7 changes: 5 additions & 2 deletions docs/design.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Design

opsforge is intentionally shell-only. The toolkit favors scripts that can
run on constrained incident-response hosts without installing a language runtime.
opsforge favors platform scripts that can run on constrained incident-response
hosts without installing a language runtime. The Linux investigator runner is
a prebuilt Rust binary for guided collection and offline reporting; it does not
require Rust on the investigated host.

## Principles

Expand All @@ -15,6 +17,7 @@ run on constrained incident-response hosts without installing a language runtime
## Layout

- `bin/` contains dispatch wrappers.
- `investigate/` contains the Linux guided collector and report generator.
- `lib/` contains shared shell and PowerShell helpers.
- `scripts/linux/` and `scripts/windows/` contain operational tools by domain.
- `configs/` contains target lists and examples.
Expand Down
70 changes: 70 additions & 0 deletions docs/linux-investigator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Linux investigator

The investigator is a separate Linux x86_64 case runner. The release bootstrap
downloads a prebuilt binary and checksum, verifies the archive, and starts
terminal setup. `--install-deps` installs missing capture tools through the
local package manager.
No Rust toolchain is needed on the investigated host. The release asset becomes
available when a new signed tag passes the release workflow.

```bash
curl -fsSL https://raw.githubusercontent.com/iamb4uc/opsforge/main/investigate/install-and-run.sh -o /tmp/opsforge-investigate-bootstrap && bash /tmp/opsforge-investigate-bootstrap --install-deps --remove-bootstrap
```

The TUI selects exfiltration review, device timeline, downloads, deep file
inventory, and live capture by default. It shows the case output directory,
asks for a location change, accepts extra log paths, and accepts positive
capture durations such as `30s`, `5m`, `1h`, or `1d`. Use `--non-interactive`
with flags for a repeatable run. A root session skips `sudo`; otherwise the
runner validates cached `sudo` access or prompts once before case creation.
It stops if elevation fails.

## Case files

Cases are created at `/var/lib/opsforge/cases/HOST-investigate-YYYYMMDD-HHMMSS/`
by default, or under a selected root-owned parent directory. Every ancestor of
the selected location must be root-owned and lack group or world write access;
this prevents an unprivileged process from redirecting root-owned case writes.
The cases contain:

- `raw/`: copied logs, browser database snapshots, command output, packet
capture, and decoded packet text.
- `normalized/events.jsonl`: one event per line, written during collection.
- `normalized/coverage.jsonl`: source status and reason, including failures,
empty sources, and unsupported formats.
- `normalized/file-inventory.jsonl`: path, size, and modification time when
deep inventory is selected.
- `manifest.jsonl`: source path, copy time, byte count, and SHA-256 for each
saved raw file.
- `case-info.json`, `completion.json`, and `checksums.sha256`: run identity,
completion marker, and hashes of the finished case files. An interrupted
case keeps saved evidence but has no completion marker.
- `findings.json`, `report.md`, `summary.txt`, and `dashboard/` with Overview,
Exfiltration, Timeline, Downloads, and Coverage HTML pages.

The dashboard opens locally without a server. It shows a bounded sample of
events so large journals do not exhaust report memory; JSONL retains every
normalized row. Keep the whole case directory when sharing it. The bootstrap
removes only its temporary files. It does not compress or remove the case.

## Sources and interpretation

The runner inventories active sockets and installed application lists, saves
the retained system journal and `/var/log` files, snapshots Firefox and
Chromium-family history, reads Chromium download records, copies supplied
logs, and captures live packets on all interfaces. The live timer starts when
the case starts. Sources are saved and hashed as they are collected.

Generic text logs are normalized line by line. Keyword matches appear as
network leads; their original lines remain in `raw/`. Browser visits are
leads, and a browser visit does not prove upload. Current sockets are observed
at one point in time. Historical application attribution depends on retained
records with application names. Packet summaries do not identify an
application by themselves. Firefox downloads, upload payloads, and binary or
compressed imported-log formats are not decoded yet; coverage records these
limits. No malware verdict or exfiltration conclusion is generated from weak
signals, so `findings.json` can be empty.

Deep inventory excludes volatile `/proc`, `/sys`, `/dev`, and `/run` trees and
the case directory itself. Raw browser databases and packet captures can
contain private data. Share only with the intended case reviewers.
1 change: 1 addition & 0 deletions investigate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target/
Loading
Loading