Skip to content

Complete native security engines and scoped policy enforcement #150

Complete native security engines and scoped policy enforcement

Complete native security engines and scoped policy enforcement #150

Workflow file for this run

name: CI
on:
push:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
# Lint levels live in `[workspace.lints]` in the root Cargo.toml so local and
# CI runs agree; don't add a blanket RUSTFLAGS here.
CARGO_TERM_COLOR: always
jobs:
rust:
name: Rust
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
with:
# This job executes repository code (cargo build/test); don't persist
# the token in git config.
persist-credentials: false
submodules: recursive
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy, llvm-tools-preview
- uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
- name: Test native coverage gate
run: python .github/scripts/check-native-coverage_tests.py
- name: Check formatting
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Build
run: cargo build --all-targets --all-features
- name: Provision Windows audit test namespace
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
# The hosted D: runner-temp ancestors fail the engine's actual owner/
# mutation ACL checks. Provision below the current user's normal profile;
# native tests still inspect and pin every ancestor, including C:\.
$auditRoot = Join-Path $env:USERPROFILE 'tinysecurity-audit-ci'
New-Item -ItemType Directory -Force $auditRoot | Out-Null
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$security = [System.Security.AccessControl.DirectorySecurity]::new()
$security.SetOwner($identity.User)
$security.SetAccessRuleProtection($true, $false)
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$identity.User,
[System.Security.AccessControl.FileSystemRights]::FullControl,
[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit',
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow
)
[void]$security.AddAccessRule($rule)
Set-Acl -LiteralPath $auditRoot -AclObject $security
"TINYSECURITY_AUDIT_TEST_ROOT=$auditRoot" >> $env:GITHUB_ENV
- name: Verify Windows module through the restricted native copy
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$libraryName = 'tinysecurity_module'
$module = "target/debug/$libraryName.dll"
$verifyRoot = Join-Path $env:RUNNER_TEMP 'tinysecurity-module-ci-verify'
New-Item -ItemType Directory -Force $verifyRoot | Out-Null
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$security = [System.Security.AccessControl.DirectorySecurity]::new()
$security.SetOwner($identity.User)
$security.SetAccessRuleProtection($true, $false)
$rights = [System.Security.AccessControl.FileSystemRights]::FullControl
$inheritance = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
$propagation = [System.Security.AccessControl.PropagationFlags]::None
$access = [System.Security.AccessControl.AccessControlType]::Allow
foreach ($sidValue in @(
$identity.User.Value,
'S-1-5-18',
'S-1-5-32-544'
)) {
$sid = [System.Security.Principal.SecurityIdentifier]::new($sidValue)
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$sid,
$rights,
$inheritance,
$propagation,
$access
)
[void]$security.AddAccessRule($rule)
}
Set-Acl -LiteralPath $verifyRoot -AclObject $security
$verifiedModule = Join-Path $verifyRoot "$libraryName.dll"
Copy-Item -LiteralPath $module -Destination $verifiedModule
# Collect independent ownership checks before aggregating failure;
# one failed check must not hide the exact installed namespace result.
$diagnosticFailure = $false
cargo test --locked --package tinysecurity-module --example audit_native_contract -- --nocapture
if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true }
cargo test --locked -p tinysecurity-audit windows_token_default_owner_explains_ordinary_created_file_owner -- --nocapture
if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true }
cargo test --locked -p tinysecurity-audit windows_new_engine_file_is_owned_by_current_user_before_any_bytes -- --nocapture
if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true }
cargo test --locked -p tinysecurity-audit windows_existing_foreign_owner_is_denied_without_owner_repair -- --nocapture
if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true }
cargo test --locked -p tinysecurity-audit windows_protected_preprovisioned_namespace_commits_exact_retry -- --nocapture
if ($LASTEXITCODE -ne 0) { $diagnosticFailure = $true }
if ($diagnosticFailure) { throw 'native audit ownership regressions failed' }
cargo run --locked --package tinysecurity-module --example verify_module -- $verifiedModule
if ($LASTEXITCODE -ne 0) { throw 'native verifier failed in workspace' }
cargo run --locked --package tinysecurity-module --example audit_native_contract -- $verifiedModule
if ($LASTEXITCODE -ne 0) { throw 'native audit verifier failed in workspace' }
$verifier = (Resolve-Path 'target/debug/examples/verify_module.exe').Path
$auditVerifier = (Resolve-Path 'target/debug/examples/audit_native_contract.exe').Path
Push-Location $env:RUNNER_TEMP
try {
& $verifier $verifiedModule
if ($LASTEXITCODE -ne 0) { throw 'native verifier failed outside workspace' }
& $auditVerifier $verifiedModule
if ($LASTEXITCODE -ne 0) { throw 'native audit verifier failed outside workspace' }
} finally {
Pop-Location
}
- name: Windows protected audit namespace and rotation behavior
if: ${{ runner.os == 'Windows' }}
run: cargo test --locked -p tinysecurity-audit windows_ -- --nocapture
- name: Test
run: cargo test --all-features
- name: Test default features
run: cargo test
# `cargo build --all-targets` only *compiles* an example. `AGENTS.md`
# promises the native loader verifier works, and a compiled
# example can still fail on its first line.
- name: Run the bundled example
if: ${{ runner.os != 'Windows' }}
shell: bash
run: |
case "$RUNNER_OS" in
Linux) module=target/debug/libtinysecurity_module.so ;;
macOS) module=target/debug/libtinysecurity_module.dylib ;;
esac
cargo run -p tinysecurity-module --example verify_module -- "$module"
cargo run -p tinysecurity-module --example audit_native_contract -- "$module"
# Installed examples must also work outside the workspace root.
module="$(pwd)/$module"
(cd target && ./debug/examples/verify_module "$module")
(cd target && ./debug/examples/audit_native_contract "$module")
# `crates/tinysecurity-bus` exists so a host can name the payload types
# without compiling the module. That promise is invisible in a diff,
# because a forbidden dependency arrives transitively through a feature
# someone enabled one crate away — so it is asserted rather than
# documented.
#
# The FORWARD form is required. `cargo tree -i <crate> -p tinysecurity-bus`
# discards the `-p` scope, prints the whole-workspace inverse tree, and
# exits 0 looking clean even when this crate is the one at fault.
- name: Assert the contract crate stays transport-free
run: |
set -euo pipefail
cargo metadata --format-version 1 --no-deps | jq -e '
[.packages[] | select(.name == "tinysecurity-bus") | .dependencies[]
| select(.kind == null) | .name] | sort == ["serde", "thiserror"]'
forbidden="$(cargo tree -p tinysecurity-bus -e normal,build --prefix none \
| grep -Ei 'tinybus|tokio|reqwest|ureq|hyper|rusqlite|git2' || true)"
if [ -n "$forbidden" ]; then
echo "tinysecurity-bus pulled in a dependency its manifest forbids:" >&2
echo "$forbidden" >&2
echo >&2
echo "The contract is what a host compiles against. It must stay free" >&2
echo "of transports, async runtimes, HTTP clients and native libraries." >&2
exit 1
fi
# Keep the existing Linux gate; Windows reports use native drive paths
# and have a separate exact-root parser and required Windows sources.
- name: Require 90% line coverage in every source file
if: ${{ runner.os == 'Linux' }}
run: .github/scripts/check-file-coverage.sh 90 coverage.json
- name: Collect native Windows coverage
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$checkoutHead = git rev-parse HEAD
if ($LASTEXITCODE -ne 0) { throw 'coverage checkout metadata failed' }
$toolchain = rustc --version --verbose
if ($LASTEXITCODE -ne 0) { throw 'coverage toolchain metadata failed' }
[ordered]@{
checkout_root = (Get-Location).Path
head = $checkoutHead
rustc = $toolchain
runner_os = $env:RUNNER_OS
} | ConvertTo-Json | Set-Content -Encoding utf8 coverage-windows-metadata.json
cargo llvm-cov --locked --workspace --all-targets --all-features --json --output-path coverage-windows.json
if ($LASTEXITCODE -ne 0) { throw 'native Windows coverage tests failed' }
cargo llvm-cov report --all-features --lcov --output-path coverage-windows.info
if ($LASTEXITCODE -ne 0) { throw 'native Windows LCOV report failed' }
- name: Require native Windows file and changed-line coverage
if: ${{ runner.os == 'Windows' }}
shell: pwsh
env:
PR_BASE: ${{ github.event.pull_request.base.sha }}
BASE_BRANCH: ${{ github.event.repository.default_branch }}
run: |
$ErrorActionPreference = 'Stop'
$base = $env:PR_BASE
if (-not $base) {
$base = git merge-base HEAD "refs/remotes/origin/$env:BASE_BRANCH"
if ($LASTEXITCODE -ne 0) { throw 'canonical base resolution failed' }
}
python .github/scripts/check-native-coverage.py --json coverage-windows.json --lcov coverage-windows.info --base $base --root (Get-Location).Path
if ($LASTEXITCODE -ne 0) { throw 'native Windows coverage gate failed' }
- name: Upload native Windows coverage reports
if: ${{ always() && runner.os == 'Windows' }}
uses: actions/upload-artifact@v7
with:
name: coverage-native-windows-${{ github.sha }}
path: |
coverage-windows.json
coverage-windows.info
coverage-windows-metadata.json
if-no-files-found: warn
- name: Upload coverage report
if: ${{ always() && runner.os == 'Linux' }}
uses: actions/upload-artifact@v7
with:
name: coverage-json-${{ matrix.os }}
path: coverage.json
if-no-files-found: ignore
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build documentation
env:
RUSTDOCFLAGS: -D warnings
run: cargo doc --no-deps --all-features
msrv:
name: Minimum supported Rust version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
submodules: recursive
# `rust-version` is inherited from `[workspace.package]`, so every member
# reports the same value. Read it off the package the module ships as
# rather than off `packages[0]`, whose order cargo does not promise.
- name: Read rust-version from Cargo.toml
id: msrv
run: |
set -euo pipefail
msrv="$(cargo metadata --format-version 1 --no-deps \
| jq -r '.packages[] | select(.name == "tinysecurity-module") | .rust_version')"
if [[ -z "$msrv" || "$msrv" == "null" ]]; then
echo "workspace.package.rust-version is not set in Cargo.toml" >&2
exit 1
fi
echo "version=$msrv" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.msrv.outputs.version }}
- uses: Swatinem/rust-cache@v2
- name: Build with the declared MSRV
run: cargo build --all-targets --all-features
supply-chain:
name: Supply chain
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
submodules: recursive
- name: Check advisories, licenses, bans, and sources
uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check all