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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ jobs:
run: go vet ./...
- name: Check shell syntax and style
run: |
sh -n runner/*.sh scripts/*.sh
shellcheck runner/*.sh scripts/*.sh
sh -n runner/*.sh scripts/*.sh testdata/tracer-failure/*.sh
shellcheck runner/*.sh scripts/*.sh testdata/tracer-failure/*.sh
- name: Validate workflows and JSON contracts
run: |
go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ All notable changes will be documented here. The project follows Keep a Changelo
9. Root owned trace channel separated from the package uid
10. Semantic JSON Schema conformance tests
11. Explicit `attestation: none` provenance boundary for unsigned profiles
12. Separate user guide, platform support guide, technical reference, and security audit record

### Security

1. Capture requires explicit experimental acknowledgement.
2. Empty, incomplete, timed out, truncated, malformed, or sentinel missing traces are rejected.
3. Package input cannot control Docker flags, images, mounts, environment names, or container names.
4. External traces require explicit acknowledgement before comparison.
5. Runner and prepared package images execute by immutable Docker content ID after resolution.
6. Any `strace` diagnostic fails capture before a trusted completion footer is emitted.
7. Runtime profile validation now enforces npm integrity, content ID, coverage, and behavior limits.
8. Path normalization applies only at genuine disposable root boundaries.
9. Docker client proxy variables are explicitly cleared in every runner container.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ check:
test -z "$$(gofmt -l cmd internal schemas)"
go vet ./...
go test -race ./...
sh -n runner/*.sh scripts/*.sh
sh -n runner/*.sh scripts/*.sh testdata/tracer-failure/*.sh
command -v shellcheck >/dev/null
shellcheck runner/*.sh scripts/*.sh
shellcheck runner/*.sh scripts/*.sh testdata/tracer-failure/*.sh
go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7
jq empty schemas/*.json testdata/npm-fixture/seed/*.json testdata/npm-fixture/seed/node_modules/behaviorlock-fixture/*.json

Expand Down
224 changes: 169 additions & 55 deletions README.md

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ Every item below must pass on a GitHub hosted Linux runner:
2. Package input cannot change the image, entrypoint, mounts, network, environment, or Docker flags.
3. Host environment, home, npm configuration, SSH material, Git configuration, cloud credentials, and Docker socket never appear inside the container or report.
4. Root filesystem writes fail and work directory writes succeed.
5. TCP, UDP, DNS, private address, host gateway, and cloud metadata attempts fail.
6. Child, grandchild, native executable, and shell activity remain observable.
7. Tracer death terminates tracees and produces `trace_incomplete`.
8. Fake syscall lines, terminal control characters, and GitHub workflow commands cannot enter the trace channel.
9. Process, memory, descriptor, file, output, syscall, and timeout exhaustion stop within limits and leave no container or image.
10. Unsupported tracing fails closed without privileged mode, disabled seccomp, host namespaces, or broad capabilities.
11. Ten repeated trusted fixture runs produce the same normalized behavior set and stable digest.
12. Every added report item points to retained raw trace evidence.
13. Trusted CI profiles carry verifiable provenance or an artifact attestation before they are used as cross-workflow policy inputs.
5. Lifecycle TCP, UDP, DNS, private address, host gateway, and cloud metadata attempts fail.
6. Acquisition uses allowlisted public registry egress or an equivalent disposable host boundary with no sensitive routes.
7. Child, grandchild, native executable, and shell activity remain observable.
8. Tracer death and tracer diagnostics terminate tracees and produce `trace_incomplete`.
9. Fake syscall lines, terminal control characters, and GitHub workflow commands cannot enter the trace channel.
10. Process, memory, descriptor, file, output, syscall, and timeout exhaustion stop within limits and leave no container or image.
11. Unsupported tracing fails closed without privileged mode, disabled seccomp, host namespaces, or broad capabilities.
12. Ten repeated trusted fixture runs produce the same normalized behavior set and stable digest.
13. Every added report item points to retained raw trace evidence.
14. Trusted CI profiles carry verifiable provenance or an artifact attestation before they are used as cross-workflow policy inputs.

## Later options

Expand Down
8 changes: 5 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ The first release accepts only an npm registry name followed by an exact semanti

Docker commands are created as argument arrays. User input is never placed inside a host shell command, container name, image name, mount, environment variable name, or Docker option.

The local runner tag is resolved to one validated Docker content ID before capture. Metadata inspection and preparation use that immutable ID. The committed package filesystem is also executed by the content ID returned from `docker commit`, not by its temporary tag.

## Observation

Capture has two disposable container phases.

The preparation phase installs an exact top level package version with lifecycle scripts disabled and records the generated dependency lock digest. It runs as uid `65532` and receives no host mounts, home directory, npm configuration, Git configuration, SSH material, cloud credentials, repository token, or Docker socket.
The preparation phase installs an exact top level package version with lifecycle scripts disabled and records the generated dependency lock digest. It runs as uid `65532` and receives no host mounts, home directory, npm configuration, Git configuration, SSH material, cloud credentials, repository token, or Docker socket. Standard uppercase and lowercase proxy variables are explicitly empty, which prevents Docker client proxy settings from being injected. Preparation still has direct registry network access, so package and transitive dependency metadata can influence outbound fetches. It belongs on a disposable runner with no route to sensitive private services.

The execution phase starts from the prepared filesystem. Networking is disabled. The root filesystem is read only and writable locations are bounded temporary filesystems. A root supervisor owns the trace channel while the package command runs as uid `65532`. After dropping all capabilities, the container adds only `SETUID`, `SETGID`, and `SYS_PTRACE` so the supervisor can perform the identity transition and trace inside the container PID namespace. The package process has zero effective capabilities. Docker's default seccomp policy remains intact.

`strace` writes into a root owned mode `0700` temporary filesystem that package code cannot access. It follows a selected set of file, process, and network syscalls. Package output is separated from the trace envelope. Root owned start and end sentinel reads and a completion footer establish basic channel integrity. Missing or malformed completion evidence makes the profile incomplete.
`strace` writes into a root owned mode `0700` temporary filesystem that package code cannot access. It follows a selected set of file, process, and network syscalls. Package output is separated from the trace envelope. Root owned start and end sentinel reads, an empty tracer diagnostic channel, and a completion footer establish basic channel integrity. Missing evidence, tracer diagnostics, timeout, truncation, or malformed completion evidence makes the profile incomplete.

## Normalization

The parser has byte, line, and behavior limits. It rejects invalid UTF 8 and unfinished syscalls. It normalizes only known disposable paths and process identifiers:
The parser has byte, line, and behavior limits. It rejects invalid UTF 8 and unfinished syscalls. It normalizes only known disposable roots at exact path boundaries and selected process identifiers:

1. `/work` becomes `$WORK`
2. `/home/scanner` becomes `$HOME`
Expand Down
2 changes: 1 addition & 1 deletion docs/DECISION_RECORD.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Known scanners already cover vulnerabilities, metadata, and static patterns. Ins

## Tradeoffs

The narrow scope produces less dramatic claims and better testability. Offline execution improves containment and reduces coverage. Docker improves accessibility but cannot provide a hostile code isolation guarantee.
The narrow scope produces less dramatic claims and better testability. Offline execution improves containment and reduces coverage. Docker improves accessibility but cannot provide a hostile code isolation guarantee. Acquisition network isolation is weaker than lifecycle isolation and must remain visible as a release blocker.

## Risk and reversibility

Expand Down
3 changes: 2 additions & 1 deletion docs/LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ BehaviorLock observes one narrow execution path. The following limits are part o
10. Failed access attempts remain observations and may create false positives.
11. A comparable profile depends on the same runner image, architecture, Node version, npm version, and harness. The generated dependency lock digest records, but does not eliminate, dependency graph variation.
12. The Debian base image is pinned, but live operating system packages installed during a runner build are not snapshot pinned. Use the exact runner image ID for comparison.
13. The acquisition phase has a wall clock limit and memory controls but no portable Docker overlay disk quota. A disposable runner remains necessary.
13. The acquisition phase has a wall clock limit and memory controls but no portable Docker overlay disk quota. It also has registry network access, and dependency metadata can influence fetch destinations. Use a disposable runner with no route to private networks, cloud metadata, or trusted services.
14. Raw traces are hashed but not retained by default. Content derived evidence identifiers are stable references to normalized records, not a substitute for raw forensic evidence.
15. An incomplete trace is an error, but a complete trace still cannot prove full coverage or prevent evasive dormant behavior.
16. Profiles can retain sensitive paths and package controlled strings. Review artifacts before sharing them.
17. Profiles are unsigned JSON. Validation checks structure and internal consistency, not authenticity. Enforcement workflows must generate profiles in a trusted job rather than accepting contributor supplied artifacts.
18. The Go CLI is tested on Linux and macOS, but full capture is verified only on GitHub hosted Linux. Native Windows and macOS tracing are not implemented.

BehaviorLock reports observations and changes. It does not report that a package is safe, clean, benign, malicious, or free of vulnerabilities.
47 changes: 47 additions & 0 deletions docs/PLATFORM_SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Platform support

BehaviorLock has two platform questions: where the CLI can run, and which operating system behavior the capture represents. They are not the same.

## Current support matrix

| Capability | Linux | macOS | Windows |
| --- | --- | --- | --- |
| Build the Go CLI | Verified | Verified | Not yet tested in CI |
| Parse an existing Linux `strace` file | Expected | Expected | Not yet tested in CI |
| Compare compatible profile JSON | Expected | Expected | Not yet tested in CI |
| Run the full capture integration | Verified on GitHub hosted Linux | Not verified | Not verified |
| Observe native operating system behavior | Linux only | No | No |

## Linux

Linux is the observed target platform for `0.1.0-dev`. The runner depends on Linux containers, Linux permissions, Linux capabilities, `/proc`, and `strace`.

The complete Docker integration runs on GitHub hosted Ubuntu. It checks the package uid, effective capabilities, trace isolation, blocked network access, canary path visibility, immutable capture evidence, and cleanup.

## macOS

The Go CLI builds and its unit tests run on GitHub hosted macOS.

The full Docker capture has not been verified on macOS. Docker Desktop could operate the Linux runner inside its virtual machine, but the result would describe Linux container behavior. It would not show native macOS file, process, or network events.

## Windows

Windows is not in the current CI matrix. The code may compile because the parser and comparison core use portable Go, but this has not been established by hosted tests.

Docker Desktop can run Linux containers on some Windows configurations. Even if capture works there, the resulting profile would describe Linux behavior rather than native Windows behavior.

## What native support would require

Native support needs a separate capture backend for each operating system.

1. Linux can continue using `strace`, with eBPF as a possible later option.
2. macOS would need an authorized event source such as Endpoint Security or another supported tracing mechanism.
3. Windows would need Windows event telemetry such as ETW plus a separate containment design.

The normalized profile and comparison model could remain shared, but every backend would need its own threat model, coverage declaration, comparability rules, tests, and release gate.

## Accurate public description

The current accurate description is:

> BehaviorLock compares observed npm install lifecycle behavior in Linux containers. Its parser and comparison core are portable, but native Windows and macOS tracing are not implemented.
105 changes: 105 additions & 0 deletions docs/SECURITY_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Security audit

## Review record

| Field | Value |
| --- | --- |
| Review date | 2026-08-08 |
| Reviewed base | `2594d5c32961cb1b55508effbe1bf5e9225e9557` |
| Scope | Go CLI, Docker orchestration, runner scripts, parser, model validation, report rendering, schemas, and GitHub Actions |
| Release status | Experimental, no tag |
| Overall decision | Suitable for continued public experimentation after the fixes in this change. Not approved as a malware sandbox or stable security product. |

## Method

The review combined:

1. Manual data flow and trust boundary review
2. Docker argument and lifecycle analysis
3. Adversarial reasoning about package controlled input
4. `gosec` across all Go packages
5. `govulncheck` at symbol level
6. `go vet`, race enabled tests, and Staticcheck
7. Bounded fuzzing of package specification and trace parsers
8. ShellCheck and shell syntax checks
9. Actionlint and manual GitHub Actions permission review
10. GitHub Dependabot, code scanning, and secret scanning alert review

No live malicious package was executed. Adversarial behavior uses inert local fixtures only.

## Fixed findings

### High: Docker client proxy configuration could enter containers

Docker can automatically populate proxy environment variables from the operator's client configuration. Proxy values may contain internal addresses or credentials. The previous Docker argument allowlist did not explicitly override them, so package code could potentially read values that the tool claimed were not inherited.

The fix explicitly sets uppercase and lowercase HTTP, HTTPS, all proxy, and no proxy variables to empty in preparation, execution, and metadata containers. Unit tests inspect every Docker argument vector, and the hosted package fixture fails if any proxy value is visible.

### Medium: mutable runner reference after evidence collection

The capture path inspected the local runner tag and recorded its image ID, but later commands still used the tag. Another local process with Docker authority could retag it between inspection and execution. A profile could then name one runner ID while execution used another image.

The fix resolves the tag once, validates the returned SHA 256 content ID, and uses that immutable ID for version inspection and preparation. The committed package filesystem is also executed by its returned content ID. Regression tests reject mutable references and invalid commit output.

### Medium: tracer diagnostics could look like a package failure

The runner relied on the `strace` exit code, which normally mirrors the traced command. A tracer failure can also return a nonzero code. Without checking the root owned diagnostic channel, some tracer failures could be reported as an ordinary lifecycle command failure instead of incomplete evidence.

The fix requires the `strace` diagnostic file to remain empty. Any diagnostic now fails capture before a trusted footer is emitted. Hosted integration builds a fake tracer that emits valid looking sentinel lines plus a diagnostic and verifies fail closed behavior.

### Low: path normalization rewrote lookalike roots

Normalization replaced `/work` and `/home/scanner` wherever those strings appeared. Paths such as `/workspace/file` could be rewritten even though they were outside the disposable root.

The fix replaces roots only when the path is exactly the root or begins with the root followed by `/`. Regression tests cover both genuine and lookalike roots.

### Low: runtime profile validation was weaker than the JSON schema

The Go validator accepted any string beginning with `sha512-` as registry integrity and did not fully bound coverage arrays or behavior counts. It also did not require a captured runner image ID to be a SHA 256 digest.

The fix validates the decoded SHA 512 length, runner image content ID, lifecycle and completeness values, coverage text, item counts, and normalized profile state. The JSON schema now carries the same count and coverage limits.

## Open design risks

### Acquisition has broad outbound network access

Preparation needs npm registry access. Lifecycle scripts are disabled, but package and transitive dependency metadata can still influence what npm fetches. The preparation container can potentially reach destinations beyond the public npm registry, including addresses reachable from the Docker bridge.

This remains a release blocker. Until an allowlisted acquisition proxy, equivalent egress control, or a stronger disposable virtual machine design exists, capture must run on an ephemeral host with no access to private networks, cloud metadata, trusted services, or credentials.

### Containers share the host kernel

The package process has no host mounts, no Docker socket, no network during lifecycle execution, and zero effective capabilities. It still shares a kernel with the Docker host or Docker Desktop virtual machine. A kernel or container runtime exploit is outside the protection offered by this harness.

### Profiles are unsigned

Anyone can edit JSON provenance fields. Structural validation cannot establish authenticity. Enforcement workflows must create both profiles in a protected trusted job and must not accept contributor supplied profiles as policy evidence.

### Observation is incomplete

The parser covers selected system calls and one install lifecycle path. It does not capture file contents, ordinary in process environment reads, all network operations, every filesystem mutation, delayed behavior, or normal package runtime.

## Tool results

| Check | Result during this review |
| --- | --- |
| `gosec` | 0 findings across 7 Go source packages |
| `govulncheck` | 0 reachable vulnerabilities |
| Staticcheck | 0 findings |
| GitHub Dependabot alerts | 0 open alerts |
| GitHub code scanning alerts | 0 open alerts |
| GitHub secret scanning alerts | 0 open alerts |
| Package specification fuzzing | No failure during the bounded local run |
| Trace parser fuzzing | No failure during the bounded local run |
| Existing hosted CodeQL | Green on reviewed base |
| Existing hosted Docker integration | Green on reviewed base |

The local Go toolchain reported vulnerabilities in standard library or imported code paths that BehaviorLock does not call. `govulncheck` found zero symbol reachable vulnerabilities in BehaviorLock. Future distributed binaries must be built with a fully patched Go toolchain.

## Release decision

This review does not open the `v0.1.0` release gate. The project may continue as an explicitly experimental source repository. A tagged release still requires every adversarial item in [ROADMAP.md](../ROADMAP.md), stronger acquisition isolation, and verifiable profile provenance.

## Reporting a new issue

Do not publish a vulnerability in a public issue. Use [GitHub private vulnerability reporting](https://github.com/kiranmagic7/behaviorlock/security/advisories/new) and follow [SECURITY.md](../SECURITY.md).
Loading
Loading