This commit is contained in:
CarterPerez-dev 2026-05-23 05:01:01 -04:00
parent 481e07bddd
commit 0006ddad3d
105 changed files with 2352 additions and 3114 deletions

15
.clang-format Normal file
View File

@ -0,0 +1,15 @@
# ©AngelaMos | 2026
# .clang-format
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 100
AccessModifierOffset: -4
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
SortIncludes: CaseSensitive
PointerAlignment: Left
SpaceBeforeParens: ControlStatements
BreakBeforeBraces: Attach

View File

@ -68,6 +68,16 @@ jobs:
- name: dlp-scanner
type: ruff
path: PROJECTS/intermediate/dlp-scanner
# Python (ruff) - Foundations
- name: hash-identifier
type: ruff
path: PROJECTS/foundations/hash-identifier
- name: http-headers-scanner
type: ruff
path: PROJECTS/foundations/http-headers-scanner
- name: password-manager
type: ruff
path: PROJECTS/foundations/password-manager
# Biome (frontend)
- name: bug-bounty-platform-frontend
type: biome
@ -84,6 +94,21 @@ jobs:
- name: encrypted-p2p-chat-frontend
type: biome
path: PROJECTS/advanced/encrypted-p2p-chat/frontend
- name: canary-token-generator-frontend
type: biome
path: PROJECTS/beginner/canary-token-generator/frontend
- name: ai-threat-detection-frontend
type: biome
path: PROJECTS/advanced/ai-threat-detection/frontend
- name: binary-analysis-tool-frontend
type: biome
path: PROJECTS/intermediate/binary-analysis-tool/frontend
- name: honeypot-network-frontend
type: biome
path: PROJECTS/advanced/honeypot-network/frontend
- name: monitor-the-situation-dashboard-frontend
type: biome
path: PROJECTS/advanced/monitor-the-situation-dashboard/frontend
# Go
- name: simple-vulnerability-scanner
type: go
@ -94,10 +119,51 @@ jobs:
- name: secrets-scanner
type: go
path: PROJECTS/intermediate/secrets-scanner
- name: canary-token-generator-backend
type: go
path: PROJECTS/beginner/canary-token-generator/backend
- name: systemd-persistence-scanner
type: go
path: PROJECTS/beginner/systemd-persistence-scanner
- name: sbom-generator-vulnerability-matcher
type: go
path: PROJECTS/intermediate/sbom-generator-vulnerability-matcher
- name: honeypot-network
type: go
path: PROJECTS/advanced/honeypot-network
- name: monitor-the-situation-dashboard-backend
type: go
path: PROJECTS/advanced/monitor-the-situation-dashboard/backend
# Nim
- name: credential-enumeration
type: nim
path: PROJECTS/intermediate/credential-enumeration
# Rust
- name: binary-analysis-tool-backend
type: rust
path: PROJECTS/intermediate/binary-analysis-tool/backend
# Crystal
- name: credential-rotation-enforcer
type: crystal
path: PROJECTS/intermediate/credential-rotation-enforcer
# V (Vlang)
- name: firewall-rule-engine
type: v
path: PROJECTS/beginner/firewall-rule-engine
# C++
- name: hash-cracker
type: cpp
path: PROJECTS/beginner/hash-cracker
- name: simple-port-scanner
type: cpp
path: PROJECTS/beginner/simple-port-scanner
- name: network-traffic-analyzer-cpp
type: cpp
path: PROJECTS/beginner/network-traffic-analyzer/cpp
# Bash (shellcheck)
- name: linux-cis-hardening-auditor
type: bash
path: PROJECTS/beginner/linux-cis-hardening-auditor
defaults:
run:
@ -168,6 +234,61 @@ jobs:
if: matrix.type == 'nim'
run: nimble install -y nph
# Rust Setup
- name: Install Rust stable
if: matrix.type == 'rust'
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Cargo registry and target
if: matrix.type == 'rust'
uses: Swatinem/rust-cache@v2
with:
workspaces: "${{ matrix.path }} -> target"
# Crystal Setup
- name: Install Crystal
if: matrix.type == 'crystal'
uses: crystal-lang/install-crystal@v1
with:
crystal: latest
- name: Cache shards
if: matrix.type == 'crystal'
uses: actions/cache@v4
with:
path: ${{ matrix.path }}/lib
key: ${{ runner.os }}-shards-${{ matrix.name }}-${{ hashFiles(format('{0}/shard.lock', matrix.path)) }}
restore-keys: |
${{ runner.os }}-shards-${{ matrix.name }}-
${{ runner.os }}-shards-
- name: Install shards
if: matrix.type == 'crystal'
run: shards install
# V (Vlang) Setup
- name: Install V
if: matrix.type == 'v'
uses: vlang/setup-v@v1.4
with:
check-latest: true
# C++ Setup
- name: Install clang-format and cppcheck
if: matrix.type == 'cpp'
run: |
sudo apt-get update -qq
sudo apt-get install -y clang-format cppcheck
# Bash Setup (shellcheck pre-installed on ubuntu-latest, but ensure latest)
- name: Install shellcheck
if: matrix.type == 'bash'
run: |
sudo apt-get update -qq
sudo apt-get install -y shellcheck
# Ruff Linting
- name: Run ruff
if: matrix.type == 'ruff'
@ -247,6 +368,152 @@ jobs:
cat nim-output.txt
continue-on-error: true
# Rust Linting
- name: Run cargo fmt and clippy
if: matrix.type == 'rust'
id: rust
run: |
RUST_OK=true
echo "Running cargo fmt --check..."
if cargo fmt -- --check > rust-output.txt 2>&1; then
echo "cargo fmt: no issues"
else
RUST_OK=false
echo "cargo fmt: formatting issues found"
fi
echo "Running cargo clippy..."
if cargo clippy --all-targets --all-features -- -D warnings >> rust-output.txt 2>&1; then
echo "cargo clippy: no issues"
else
RUST_OK=false
echo "cargo clippy: issues found"
fi
if [[ "$RUST_OK" == "true" ]]; then
echo "RUST_PASSED=true" >> $GITHUB_ENV
else
echo "RUST_PASSED=false" >> $GITHUB_ENV
fi
cat rust-output.txt
continue-on-error: true
# Crystal Linting
- name: Run crystal format + ameba + compile check
if: matrix.type == 'crystal'
id: crystal
run: |
CRYSTAL_OK=true
echo "Running crystal tool format --check..."
if crystal tool format --check src/ spec/ > crystal-output.txt 2>&1; then
echo "crystal format: no issues"
else
CRYSTAL_OK=false
echo "crystal format: formatting issues found"
fi
echo "Running ameba..."
if bin/ameba src/ >> crystal-output.txt 2>&1; then
echo "ameba: no issues"
else
CRYSTAL_OK=false
echo "ameba: issues found"
fi
echo "Running crystal build --no-codegen..."
if crystal build --no-codegen src/cre.cr >> crystal-output.txt 2>&1; then
echo "crystal build: passed"
else
CRYSTAL_OK=false
echo "crystal build: failed"
fi
if [[ "$CRYSTAL_OK" == "true" ]]; then
echo "CRYSTAL_PASSED=true" >> $GITHUB_ENV
else
echo "CRYSTAL_PASSED=false" >> $GITHUB_ENV
fi
cat crystal-output.txt
continue-on-error: true
# V Linting
- name: Run v fmt and v vet
if: matrix.type == 'v'
id: v
run: |
V_OK=true
echo "Running v fmt -diff src/..."
v fmt -diff src/ > v-output.txt 2>&1 || true
echo "Running v fmt -verify src/..."
if v fmt -verify src/ >> v-output.txt 2>&1; then
echo "v fmt: no issues"
else
V_OK=false
echo "v fmt: formatting issues found"
fi
echo "Running v vet src/..."
if v vet src/ >> v-output.txt 2>&1; then
echo "v vet: no issues"
else
V_OK=false
echo "v vet: issues found"
fi
if [[ "$V_OK" == "true" ]]; then
echo "V_PASSED=true" >> $GITHUB_ENV
else
echo "V_PASSED=false" >> $GITHUB_ENV
fi
cat v-output.txt
continue-on-error: true
# C++ Linting
- name: Run clang-format and cppcheck
if: matrix.type == 'cpp'
id: cpp
run: |
CPP_OK=true
echo "Running clang-format check..."
FILES=$(find . \( -path ./build -o -path ./cmake-build -o -path ./node_modules -o -path ./.git \) -prune -o \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' \) -print 2>/dev/null)
if [ -n "$FILES" ]; then
if echo "$FILES" | xargs clang-format --dry-run --Werror > cpp-output.txt 2>&1; then
echo "clang-format: no issues"
else
CPP_OK=false
echo "clang-format: formatting issues found"
fi
else
echo "clang-format: no C++ files found" > cpp-output.txt
fi
echo "Running cppcheck..."
if cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q . >> cpp-output.txt 2>&1; then
echo "cppcheck: no issues"
else
CPP_OK=false
echo "cppcheck: issues found"
fi
if [[ "$CPP_OK" == "true" ]]; then
echo "CPP_PASSED=true" >> $GITHUB_ENV
else
echo "CPP_PASSED=false" >> $GITHUB_ENV
fi
cat cpp-output.txt
continue-on-error: true
# Bash Linting
- name: Run shellcheck
if: matrix.type == 'bash'
id: bash
run: |
echo "Running shellcheck..."
FILES=$(find . -name '*.sh' -not -path '*/.venv/*' -not -path '*/node_modules/*' 2>/dev/null || true)
if [ -z "$FILES" ]; then
echo "shellcheck: no .sh files found" > bash-output.txt
echo "BASH_PASSED=true" >> $GITHUB_ENV
elif echo "$FILES" | xargs shellcheck > bash-output.txt 2>&1; then
echo "BASH_PASSED=true" >> $GITHUB_ENV
echo "shellcheck: no issues"
else
echo "BASH_PASSED=false" >> $GITHUB_ENV
echo "shellcheck: issues found"
fi
cat bash-output.txt
continue-on-error: true
# Create Summary for Ruff
- name: Create Ruff Lint Summary
if: matrix.type == 'ruff'
@ -371,6 +638,151 @@ jobs:
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for Rust
- name: Create Rust Lint Summary
if: matrix.type == 'rust'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.RUST_PASSED }}" == "true" ]]; then
echo '### cargo fmt + clippy: **Passed**'
echo 'No Rust issues found.'
else
echo '### cargo fmt + clippy: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 rust-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.RUST_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for Crystal
- name: Create Crystal Lint Summary
if: matrix.type == 'crystal'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.CRYSTAL_PASSED }}" == "true" ]]; then
echo '### crystal format + ameba + build: **Passed**'
echo 'No Crystal issues found.'
else
echo '### crystal format + ameba + build: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 crystal-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.CRYSTAL_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for V
- name: Create V Lint Summary
if: matrix.type == 'v'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.V_PASSED }}" == "true" ]]; then
echo '### v fmt + v vet: **Passed**'
echo 'No V issues found.'
else
echo '### v fmt + v vet: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 v-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.V_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for C++
- name: Create C++ Lint Summary
if: matrix.type == 'cpp'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.CPP_PASSED }}" == "true" ]]; then
echo '### clang-format + cppcheck: **Passed**'
echo 'No C++ issues found.'
else
echo '### clang-format + cppcheck: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 cpp-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.CPP_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Create Summary for Bash
- name: Create Bash Lint Summary
if: matrix.type == 'bash'
run: |
{
echo "## Lint Results: ${{ matrix.name }}"
echo ''
if [[ "${{ env.BASH_PASSED }}" == "true" ]]; then
echo '### shellcheck: **Passed**'
echo 'No shell issues found.'
else
echo '### shellcheck: **Issues Found**'
echo '<details><summary>View output</summary>'
echo ''
echo '```'
head -100 bash-output.txt
echo '```'
echo '</details>'
fi
echo ''
if [[ "${{ env.BASH_PASSED }}" == "true" ]]; then
echo '---'
echo '### All checks passed!'
else
echo '---'
echo '### Review the issues above'
fi
} >> $GITHUB_STEP_SUMMARY
# Exit with proper status
- name: Check lint status
run: |
@ -394,5 +806,30 @@ jobs:
echo "Nim lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "rust" ]]; then
if [[ "${{ env.RUST_PASSED }}" == "false" ]]; then
echo "Rust lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "crystal" ]]; then
if [[ "${{ env.CRYSTAL_PASSED }}" == "false" ]]; then
echo "Crystal lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "v" ]]; then
if [[ "${{ env.V_PASSED }}" == "false" ]]; then
echo "V lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "cpp" ]]; then
if [[ "${{ env.CPP_PASSED }}" == "false" ]]; then
echo "C++ lint checks failed"
exit 1
fi
elif [[ "${{ matrix.type }}" == "bash" ]]; then
if [[ "${{ env.BASH_PASSED }}" == "false" ]]; then
echo "Bash lint checks failed"
exit 1
fi
fi
echo "All lint checks passed"

View File

@ -84,6 +84,37 @@ repos:
files: ^PROJECTS/advanced/ai-threat-detection/backend/
exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (dlp-scanner)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/intermediate/dlp-scanner/
exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (linux-ebpf-security-tracer)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/beginner/linux-ebpf-security-tracer/
exclude: (\.venv|__pycache__|\.pytest_cache)/
# Foundations projects
- id: ruff
name: ruff check (hash-identifier)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/foundations/hash-identifier/
exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (http-headers-scanner)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/foundations/http-headers-scanner/
exclude: (\.venv|__pycache__|\.pytest_cache)/
- id: ruff
name: ruff check (password-manager)
args: [--fix, --exit-non-zero-on-fix]
files: ^PROJECTS/foundations/password-manager/
exclude: (\.venv|__pycache__|\.pytest_cache)/
# Go golangci-lint Checks
- repo: local
@ -112,6 +143,48 @@ repos:
types: [go]
pass_filenames: false
- id: golangci-lint-canary-token-generator
name: golangci-lint (canary-token-generator backend)
entry: bash -c 'cd PROJECTS/beginner/canary-token-generator/backend && golangci-lint run --fix'
language: system
files: ^PROJECTS/beginner/canary-token-generator/backend/
types: [go]
pass_filenames: false
- id: golangci-lint-systemd-persistence-scanner
name: golangci-lint (systemd-persistence-scanner)
entry: bash -c 'cd PROJECTS/beginner/systemd-persistence-scanner && golangci-lint run --fix'
language: system
files: ^PROJECTS/beginner/systemd-persistence-scanner/
types: [go]
pass_filenames: false
- id: golangci-lint-sbom-generator
name: golangci-lint (sbom-generator-vulnerability-matcher)
entry: bash -c 'cd PROJECTS/intermediate/sbom-generator-vulnerability-matcher && golangci-lint run --fix'
language: system
files: ^PROJECTS/intermediate/sbom-generator-vulnerability-matcher/
exclude: ^PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/
types: [go]
pass_filenames: false
- id: golangci-lint-honeypot-network
name: golangci-lint (honeypot-network)
entry: bash -c 'cd PROJECTS/advanced/honeypot-network && golangci-lint run --fix'
language: system
files: ^PROJECTS/advanced/honeypot-network/
exclude: ^PROJECTS/advanced/honeypot-network/frontend/
types: [go]
pass_filenames: false
- id: golangci-lint-monitor-the-situation-dashboard
name: golangci-lint (monitor-the-situation-dashboard backend)
entry: bash -c 'cd PROJECTS/advanced/monitor-the-situation-dashboard/backend && golangci-lint run --fix'
language: system
files: ^PROJECTS/advanced/monitor-the-situation-dashboard/backend/
types: [go]
pass_filenames: false
# Biome Frontend Checks
- repo: local
hooks:
@ -150,6 +223,41 @@ repos:
files: ^PROJECTS/advanced/encrypted-p2p-chat/frontend/src/
pass_filenames: false
- id: biome-canary-token-generator
name: biome check (canary-token-generator frontend)
entry: bash -c 'cd PROJECTS/beginner/canary-token-generator/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/beginner/canary-token-generator/frontend/src/
pass_filenames: false
- id: biome-ai-threat-detection
name: biome check (ai-threat-detection frontend)
entry: bash -c 'cd PROJECTS/advanced/ai-threat-detection/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/advanced/ai-threat-detection/frontend/src/
pass_filenames: false
- id: biome-binary-analysis-tool
name: biome check (binary-analysis-tool frontend)
entry: bash -c 'cd PROJECTS/intermediate/binary-analysis-tool/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/intermediate/binary-analysis-tool/frontend/src/
pass_filenames: false
- id: biome-honeypot-network
name: biome check (honeypot-network frontend)
entry: bash -c 'cd PROJECTS/advanced/honeypot-network/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/advanced/honeypot-network/frontend/src/
pass_filenames: false
- id: biome-monitor-the-situation-dashboard
name: biome check (monitor-the-situation-dashboard frontend)
entry: bash -c 'cd PROJECTS/advanced/monitor-the-situation-dashboard/frontend && npx @biomejs/biome check .'
language: system
files: ^PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/
pass_filenames: false
# Nim nph Checks
- repo: local
hooks:
@ -160,6 +268,130 @@ repos:
files: ^PROJECTS/intermediate/credential-enumeration/src/
pass_filenames: false
# Rust Checks (cargo fmt + clippy)
- repo: local
hooks:
- id: cargo-fmt-binary-analysis-tool
name: cargo fmt (binary-analysis-tool backend)
entry: bash -c 'cd PROJECTS/intermediate/binary-analysis-tool/backend && cargo fmt -- --check'
language: system
files: ^PROJECTS/intermediate/binary-analysis-tool/backend/
types: [rust]
pass_filenames: false
- id: cargo-clippy-binary-analysis-tool
name: cargo clippy (binary-analysis-tool backend)
entry: bash -c 'cd PROJECTS/intermediate/binary-analysis-tool/backend && cargo clippy --all-targets --all-features -- -D warnings'
language: system
files: ^PROJECTS/intermediate/binary-analysis-tool/backend/
types: [rust]
pass_filenames: false
# Crystal Checks (format + ameba + compile)
- repo: local
hooks:
- id: crystal-format-credential-rotation-enforcer
name: crystal format check (credential-rotation-enforcer)
entry: bash -c 'cd PROJECTS/intermediate/credential-rotation-enforcer && crystal tool format --check src/ spec/'
language: system
files: ^PROJECTS/intermediate/credential-rotation-enforcer/
types_or: [crystal]
pass_filenames: false
- id: ameba-credential-rotation-enforcer
name: ameba (credential-rotation-enforcer)
entry: bash -c 'cd PROJECTS/intermediate/credential-rotation-enforcer && bin/ameba src/'
language: system
files: ^PROJECTS/intermediate/credential-rotation-enforcer/
types_or: [crystal]
pass_filenames: false
- id: crystal-build-check-credential-rotation-enforcer
name: crystal compile check (credential-rotation-enforcer)
entry: bash -c 'cd PROJECTS/intermediate/credential-rotation-enforcer && crystal build --no-codegen src/cre.cr'
language: system
files: ^PROJECTS/intermediate/credential-rotation-enforcer/
types_or: [crystal]
pass_filenames: false
# V (Vlang) Checks
- repo: local
hooks:
- id: v-fmt-firewall-rule-engine
name: v fmt (firewall-rule-engine)
entry: bash -c 'cd PROJECTS/beginner/firewall-rule-engine && v fmt -verify src/'
language: system
files: ^PROJECTS/beginner/firewall-rule-engine/src/
pass_filenames: false
- id: v-vet-firewall-rule-engine
name: v vet (firewall-rule-engine)
entry: bash -c 'cd PROJECTS/beginner/firewall-rule-engine && v vet src/'
language: system
files: ^PROJECTS/beginner/firewall-rule-engine/src/
pass_filenames: false
# C++ Checks (clang-format + cppcheck)
- repo: local
hooks:
- id: clang-format-hash-cracker
name: clang-format (hash-cracker)
entry: bash -c 'cd PROJECTS/beginner/hash-cracker && find . \( -path ./build -o -path ./cmake-build -o -path ./.git \) -prune -o \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) -print 2>/dev/null | xargs -r clang-format --dry-run --Werror'
language: system
files: ^PROJECTS/beginner/hash-cracker/
types_or: [c++, c]
pass_filenames: false
- id: cppcheck-hash-cracker
name: cppcheck (hash-cracker)
entry: bash -c 'cd PROJECTS/beginner/hash-cracker && cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q .'
language: system
files: ^PROJECTS/beginner/hash-cracker/
types_or: [c++, c]
pass_filenames: false
- id: clang-format-simple-port-scanner
name: clang-format (simple-port-scanner)
entry: bash -c 'cd PROJECTS/beginner/simple-port-scanner && find . \( -path ./build -o -path ./cmake-build -o -path ./.git \) -prune -o \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) -print 2>/dev/null | xargs -r clang-format --dry-run --Werror'
language: system
files: ^PROJECTS/beginner/simple-port-scanner/
types_or: [c++, c]
pass_filenames: false
- id: cppcheck-simple-port-scanner
name: cppcheck (simple-port-scanner)
entry: bash -c 'cd PROJECTS/beginner/simple-port-scanner && cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q .'
language: system
files: ^PROJECTS/beginner/simple-port-scanner/
types_or: [c++, c]
pass_filenames: false
- id: clang-format-network-traffic-analyzer-cpp
name: clang-format (network-traffic-analyzer/cpp)
entry: bash -c 'cd PROJECTS/beginner/network-traffic-analyzer/cpp && find . \( -path ./build -o -path ./cmake-build -o -path ./.git \) -prune -o \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) -print 2>/dev/null | xargs -r clang-format --dry-run --Werror'
language: system
files: ^PROJECTS/beginner/network-traffic-analyzer/cpp/
types_or: [c++, c]
pass_filenames: false
- id: cppcheck-network-traffic-analyzer-cpp
name: cppcheck (network-traffic-analyzer/cpp)
entry: bash -c 'cd PROJECTS/beginner/network-traffic-analyzer/cpp && cppcheck --enable=warning,style,performance,portability --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches -i build -i cmake-build -q .'
language: system
files: ^PROJECTS/beginner/network-traffic-analyzer/cpp/
types_or: [c++, c]
pass_filenames: false
# Bash (shellcheck)
- repo: local
hooks:
- id: shellcheck-linux-cis-hardening-auditor
name: shellcheck (linux-cis-hardening-auditor)
entry: bash -c 'find PROJECTS/beginner/linux-cis-hardening-auditor -name "*.sh" -not -path "*/.venv/*" | xargs -r shellcheck'
language: system
files: ^PROJECTS/beginner/linux-cis-hardening-auditor/.*\.sh$
pass_filenames: false
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:

View File

@ -2,8 +2,8 @@
// event-feed.tsx
import { useWebSocketStore } from '@/core/lib/websocket.store'
import { ServiceBadge } from './service-badge'
import styles from './event-feed.module.scss'
import { ServiceBadge } from './service-badge'
const MAX_VISIBLE = 15

View File

@ -9,9 +9,5 @@ interface ServiceBadgeProps {
}
export function ServiceBadge({ service }: ServiceBadgeProps) {
return (
<span className={styles.badge}>
{service.toUpperCase()}
</span>
)
return <span className={styles.badge}>{service.toUpperCase()}</span>
}

View File

@ -52,9 +52,7 @@ export function Shell() {
<div className={styles.status}>
<span className={styles.statusLabel}>LINK</span>
<span
className={connected ? styles.statusOn : styles.statusOff}
>
<span className={connected ? styles.statusOn : styles.statusOff}>
{connected ? 'ACTIVE' : 'DOWN'}
</span>
</div>

View File

@ -57,9 +57,7 @@ export function AttackerDetailPage() {
<div className={styles.dossierRow}>
<span className={styles.dossierLabel}>THREAT SCORE</span>
<span
className={`${styles.dossierValue} ${styles.threatValue}`}
>
<span className={`${styles.dossierValue} ${styles.threatValue}`}>
{attacker.threat_score}
</span>
</div>

View File

@ -34,10 +34,7 @@ export function AttackersPage() {
{attackers?.map((a) => (
<tr key={a.id}>
<td>
<Link
to={`/attackers/${a.id}`}
className={styles.link}
>
<Link to={`/attackers/${a.id}`} className={styles.link}>
{a.ip}
</Link>
</td>

View File

@ -113,11 +113,7 @@ export function DashboardPage() {
axisLine={false}
/>
<Tooltip contentStyle={TOOLTIP_STYLE} cursor={false} />
<Bar
dataKey="count"
fill="oklch(70% 0.19 55)"
radius={0}
/>
<Bar dataKey="count" fill="oklch(70% 0.19 55)" radius={0} />
</BarChart>
</ResponsiveContainer>
</div>
@ -143,9 +139,7 @@ export function DashboardPage() {
<section className={styles.panel}>
<h2 className={styles.panelLabel}>Top Usernames</h2>
<div className={styles.list}>
{credentials.top_usernames
.slice(0, TOP_CREDS_LIMIT)
.map((u) => (
{credentials.top_usernames.slice(0, TOP_CREDS_LIMIT).map((u) => (
<div key={u.value} className={styles.listRow}>
<span className={styles.listLabel}>{u.value}</span>
<span className={styles.listValue}>{u.count}</span>
@ -157,9 +151,7 @@ export function DashboardPage() {
<section className={styles.panel}>
<h2 className={styles.panelLabel}>Top Passwords</h2>
<div className={styles.list}>
{credentials.top_passwords
.slice(0, TOP_CREDS_LIMIT)
.map((p) => (
{credentials.top_passwords.slice(0, TOP_CREDS_LIMIT).map((p) => (
<div key={p.value} className={styles.listRow}>
<span className={styles.listLabel}>{p.value}</span>
<span className={styles.listValue}>{p.count}</span>

View File

@ -10,10 +10,7 @@ const PAGE_SIZE = 50
export function EventsPage() {
const [ipFilter, setIpFilter] = useState('')
const { data: events, isLoading } = useEvents(
PAGE_SIZE,
ipFilter || undefined
)
const { data: events, isLoading } = useEvents(PAGE_SIZE, ipFilter || undefined)
return (
<div className={styles.page}>

View File

@ -27,24 +27,16 @@ export function IntelPage() {
const res = await apiClient.get(API_ENDPOINTS.IOCS.EXPORT_STIX, {
responseType: 'text',
})
downloadBlob(
res.data as string,
'hive-iocs.stix.json',
'application/json'
)
downloadBlob(res.data as string, 'hive-iocs.stix.json', 'application/json')
}
async function exportBlocklist(format: string) {
const res = await apiClient.get(
API_ENDPOINTS.IOCS.EXPORT_BLOCKLIST,
{ params: { format }, responseType: 'text' }
)
const res = await apiClient.get(API_ENDPOINTS.IOCS.EXPORT_BLOCKLIST, {
params: { format },
responseType: 'text',
})
const ext = format === 'csv' ? '.csv' : '.txt'
downloadBlob(
res.data as string,
`hive-blocklist${ext}`,
'text/plain'
)
downloadBlob(res.data as string, `hive-blocklist${ext}`, 'text/plain')
}
return (
@ -52,18 +44,12 @@ export function IntelPage() {
<header className={styles.heading}>
<div className={styles.headingLeft}>
<h1 className={styles.title}>Intel</h1>
<span className={styles.subtitle}>
THREAT INTELLIGENCE PRODUCTS
</span>
<span className={styles.subtitle}>THREAT INTELLIGENCE PRODUCTS</span>
</div>
<div className={styles.exports}>
<span className={styles.exportLabel}>EXPORT</span>
<button
type="button"
className={styles.exportBtn}
onClick={exportSTIX}
>
<button type="button" className={styles.exportBtn} onClick={exportSTIX}>
STIX 2.1
</button>
{BLOCKLIST_FORMATS.map((fmt) => (
@ -103,12 +89,8 @@ export function IntelPage() {
<td>{ioc.confidence}%</td>
<td>{ioc.sight_count}</td>
<td>{ioc.source}</td>
<td>
{new Date(ioc.first_seen).toLocaleDateString()}
</td>
<td>
{new Date(ioc.last_seen).toLocaleDateString()}
</td>
<td>{new Date(ioc.first_seen).toLocaleDateString()}</td>
<td>{new Date(ioc.last_seen).toLocaleDateString()}</td>
</tr>
))}
</tbody>
@ -120,25 +102,20 @@ export function IntelPage() {
className={styles.pageBtn}
disabled={offset === 0}
onClick={() =>
setOffset(
Math.max(0, offset - PAGINATION.DEFAULT_LIMIT)
)
setOffset(Math.max(0, offset - PAGINATION.DEFAULT_LIMIT))
}
>
&#9664; PREV
</button>
<span className={styles.pageInfo}>
{offset + 1}&ndash;
{Math.min(offset + PAGINATION.DEFAULT_LIMIT, total)} OF{' '}
{total}
{Math.min(offset + PAGINATION.DEFAULT_LIMIT, total)} OF {total}
</span>
<button
type="button"
className={styles.pageBtn}
disabled={offset + PAGINATION.DEFAULT_LIMIT >= total}
onClick={() =>
setOffset(offset + PAGINATION.DEFAULT_LIMIT)
}
onClick={() => setOffset(offset + PAGINATION.DEFAULT_LIMIT)}
>
NEXT &#9654;
</button>

View File

@ -29,9 +29,7 @@ export function MitrePage() {
const { data: techniques } = useMitreTechniques()
const { data: heatmap } = useMitreHeatmap()
const countMap = new Map(
heatmap?.map((h) => [h.technique_id, h.count])
)
const countMap = new Map(heatmap?.map((h) => [h.technique_id, h.count]))
const byTactic = new Map<string, typeof techniques>()
for (const t of techniques ?? []) {
@ -51,9 +49,7 @@ export function MitrePage() {
<span className={styles.legendLabel}>INTENSITY</span>
{HEAT_THRESHOLDS.map((t, i) => (
<span key={t} className={styles.legendItem}>
<span
className={`${styles.legendSwatch} ${styles[`heat${i}`]}`}
/>
<span className={`${styles.legendSwatch} ${styles[`heat${i}`]}`} />
<span className={styles.legendText}>{t}+</span>
</span>
))}
@ -66,9 +62,7 @@ export function MitrePage() {
return (
<div key={tactic} className={styles.tacticColumn}>
<h3 className={styles.tacticLabel}>
{tactic.replace(/-/g, ' ')}
</h3>
<h3 className={styles.tacticLabel}>{tactic.replace(/-/g, ' ')}</h3>
<div className={styles.techniques}>
{techs.map((t) => {
const count = countMap.get(t.id) ?? 0
@ -83,9 +77,7 @@ export function MitrePage() {
<span className={styles.techId}>{t.id}</span>
<span className={styles.techName}>{t.name}</span>
{count > 0 && (
<span className={styles.techCount}>
{count}
</span>
<span className={styles.techCount}>{count}</span>
)}
</div>
)

View File

@ -44,9 +44,7 @@ export function SessionDetailPage() {
&#8592; SESSIONS
</Link>
<h1 className={styles.detailTitle}>
SESSION {session.id.slice(0, 8)}
</h1>
<h1 className={styles.detailTitle}>SESSION {session.id.slice(0, 8)}</h1>
<div className={styles.dossier}>
{rows.map((row) => (

View File

@ -52,10 +52,7 @@ export function SessionsPage() {
return (
<tr key={s.id}>
<td>
<Link
to={`/sessions/${s.id}`}
className={styles.link}
>
<Link to={`/sessions/${s.id}`} className={styles.link}>
{s.id.slice(0, 8)}
</Link>
</td>
@ -67,9 +64,7 @@ export function SessionsPage() {
<td>{s.command_count}</td>
<td className={styles.threat}>{s.threat_score}</td>
<td>{new Date(s.started_at).toLocaleString()}</td>
<td>
{duration !== null ? `${duration}s` : 'active'}
</td>
<td>{duration !== null ? `${duration}s` : 'active'}</td>
</tr>
)
})}
@ -81,15 +76,12 @@ export function SessionsPage() {
type="button"
className={styles.pageBtn}
disabled={offset === 0}
onClick={() =>
setOffset(Math.max(0, offset - PAGE_SIZE))
}
onClick={() => setOffset(Math.max(0, offset - PAGE_SIZE))}
>
&#9664; PREV
</button>
<span className={styles.pageInfo}>
{offset + 1}&ndash;{Math.min(offset + PAGE_SIZE, total)}{' '}
OF {total}
{offset + 1}&ndash;{Math.min(offset + PAGE_SIZE, total)} OF {total}
</span>
<button
type="button"

View File

@ -9,8 +9,8 @@ import {
type AlertRule,
useAlertRules,
useChangePassword,
useLogout,
useDeleteAlertRule,
useLogout,
useUpdateAlertRule,
useUpdateEmail,
useUpdateProfile,

View File

@ -15,13 +15,18 @@ const (
)
func main() {
os.Exit(run())
}
func run() int {
client := &http.Client{Timeout: httpDialTO}
resp, err := client.Get(healthURL)
if err != nil {
os.Exit(1)
return 1
}
defer func() { _ = resp.Body.Close() }()
defer func() { _ = resp.Body.Close() }() //nolint:errcheck // healthcheck binary; close error not actionable
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
os.Exit(1)
return 1
}
return 0
}

View File

@ -29,11 +29,6 @@ Connects to:
attack/RuleAttack.hpp - RuleAttack for mutation mode
*/
#include <boost/program_options.hpp>
#include <expected>
#include <iostream>
#include <print>
#include <string>
#include "src/attack/BruteForceAttack.hpp"
#include "src/attack/DictionaryAttack.hpp"
#include "src/attack/RuleAttack.hpp"
@ -45,20 +40,31 @@ Connects to:
#include "src/hash/SHA1Hasher.hpp"
#include "src/hash/SHA256Hasher.hpp"
#include "src/hash/SHA512Hasher.hpp"
#include <boost/program_options.hpp>
#include <expected>
#include <iostream>
#include <print>
#include <string>
namespace po = boost::program_options;
static std::string build_charset(const std::string &spec) {
std::string result;
auto has = [&](std::string_view token) {
return spec.find(token) != std::string::npos;
};
auto has = [&](std::string_view token) { return spec.find(token) != std::string::npos; };
if (has("lower")) { result += config::CHARSET_LOWER; }
if (has("upper")) { result += config::CHARSET_UPPER; }
if (has("digits")) { result += config::CHARSET_DIGITS; }
if (has("special")) { result += config::CHARSET_SPECIAL; }
if (has("lower")) {
result += config::CHARSET_LOWER;
}
if (has("upper")) {
result += config::CHARSET_UPPER;
}
if (has("digits")) {
result += config::CHARSET_DIGITS;
}
if (has("special")) {
result += config::CHARSET_SPECIAL;
}
if (result.empty()) {
result += config::CHARSET_LOWER;
@ -69,8 +75,7 @@ static std::string build_charset(const std::string& spec) {
}
template <Hasher H>
static auto dispatch_attack(const CrackConfig& cfg)
-> std::expected<CrackResult, CrackError> {
static auto dispatch_attack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError> {
if (cfg.bruteforce) {
return Engine::crack<H, BruteForceAttack>(cfg);
}
@ -83,10 +88,14 @@ static auto dispatch_attack(const CrackConfig& cfg)
static auto dispatch_hasher(HashType type, const CrackConfig &cfg)
-> std::expected<CrackResult, CrackError> {
switch (type) {
case HashType::MD5: return dispatch_attack<MD5Hasher>(cfg);
case HashType::SHA1: return dispatch_attack<SHA1Hasher>(cfg);
case HashType::SHA256: return dispatch_attack<SHA256Hasher>(cfg);
case HashType::SHA512: return dispatch_attack<SHA512Hasher>(cfg);
case HashType::MD5:
return dispatch_attack<MD5Hasher>(cfg);
case HashType::SHA1:
return dispatch_attack<SHA1Hasher>(cfg);
case HashType::SHA256:
return dispatch_attack<SHA256Hasher>(cfg);
case HashType::SHA512:
return dispatch_attack<SHA512Hasher>(cfg);
}
return std::unexpected(CrackError::UnsupportedAlgorithm);
}
@ -96,21 +105,34 @@ static std::string json_escape(std::string_view s) {
result.reserve(s.size());
for (char c : s) {
switch (c) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default: result += c; break;
case '"':
result += "\\\"";
break;
case '\\':
result += "\\\\";
break;
case '\n':
result += "\\n";
break;
case '\r':
result += "\\r";
break;
case '\t':
result += "\\t";
break;
default:
result += c;
break;
}
}
return result;
}
static void write_json_result(const std::string& path,
const CrackResult& result) {
static void write_json_result(const std::string &path, const CrackResult &result) {
auto *f = std::fopen(path.c_str(), "w");
if (!f) { return; }
if (!f) {
return;
}
std::fprintf(f,
"{\n"
@ -121,37 +143,33 @@ static void write_json_result(const std::string& path,
" \"candidates_tested\": %zu,\n"
" \"hashes_per_second\": %.2f\n"
"}\n",
json_escape(result.plaintext).c_str(),
json_escape(result.hash).c_str(),
json_escape(result.algorithm).c_str(),
result.elapsed_seconds,
result.candidates_tested,
result.hashes_per_second);
json_escape(result.plaintext).c_str(), json_escape(result.hash).c_str(),
json_escape(result.algorithm).c_str(), result.elapsed_seconds,
result.candidates_tested, result.hashes_per_second);
std::fclose(f);
}
int main(int argc, char *argv[]) {
po::options_description desc("hashcracker - Multi-threaded hash cracking tool");
desc.add_options()
("help,h", "Show help message")
("hash", po::value<std::string>(), "Target hash to crack")
("type", po::value<std::string>()->default_value("auto"),
"Hash type: md5, sha1, sha256, sha512, auto")
("wordlist,w", po::value<std::string>(), "Path to wordlist file")
("bruteforce,b", "Use brute-force attack mode")
("charset", po::value<std::string>()->default_value("lower,digits"),
"Character sets: lower,upper,digits,special")
("max-length", po::value<std::size_t>()->default_value(
config::DEFAULT_MAX_BRUTE_LENGTH), "Max password length for brute-force")
("rules,r", "Apply mutation rules to dictionary words")
("chain-rules", "Chain mutation rules in combination")
("salt", po::value<std::string>(), "Salt value to prepend/append")
("salt-position", po::value<std::string>()->default_value("prepend"),
"Salt position: prepend or append")
("threads,t", po::value<unsigned>()->default_value(
config::DEFAULT_THREAD_COUNT), "Thread count (0 = auto)")
("output,o", po::value<std::string>(), "Write JSON result to file");
desc.add_options()("help,h", "Show help message")("hash", po::value<std::string>(),
"Target hash to crack")(
"type", po::value<std::string>()->default_value("auto"),
"Hash type: md5, sha1, sha256, sha512, auto")("wordlist,w", po::value<std::string>(),
"Path to wordlist file")(
"bruteforce,b", "Use brute-force attack mode")(
"charset", po::value<std::string>()->default_value("lower,digits"),
"Character sets: lower,upper,digits,special")(
"max-length", po::value<std::size_t>()->default_value(config::DEFAULT_MAX_BRUTE_LENGTH),
"Max password length for brute-force")("rules,r",
"Apply mutation rules to dictionary words")(
"chain-rules", "Chain mutation rules in combination")("salt", po::value<std::string>(),
"Salt value to prepend/append")(
"salt-position", po::value<std::string>()->default_value("prepend"),
"Salt position: prepend or append")(
"threads,t", po::value<unsigned>()->default_value(config::DEFAULT_THREAD_COUNT),
"Thread count (0 = auto)")("output,o", po::value<std::string>(),
"Write JSON result to file");
po::variables_map vm;
try {
@ -198,8 +216,7 @@ int main(int argc, char* argv[]) {
if (cfg.hash_type == "auto") {
auto detected = HashDetector::detect(cfg.target_hash);
if (!detected.has_value()) {
std::println(stderr, "Error: {}",
crack_error_message(detected.error()));
std::println(stderr, "Error: {}", crack_error_message(detected.error()));
return 1;
}
hash_type = *detected;

View File

@ -26,8 +26,7 @@ Connects to:
#include "src/attack/BruteForceAttack.hpp"
#include <algorithm>
std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size,
std::size_t max_length) {
std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size, std::size_t max_length) {
std::size_t total = 0;
std::size_t power = 1;
for (std::size_t len = 1; len <= max_length; ++len) {
@ -37,17 +36,15 @@ std::size_t BruteForceAttack::compute_keyspace(std::size_t charset_size,
return total;
}
BruteForceAttack::BruteForceAttack(std::string_view charset,
std::size_t max_length,
unsigned thread_index,
unsigned total_threads)
BruteForceAttack::BruteForceAttack(std::string_view charset, std::size_t max_length,
unsigned thread_index, unsigned total_threads)
: charset_(charset), max_length_(max_length),
total_keyspace_(compute_keyspace(charset.size(), max_length)) {
std::size_t per_thread = total_keyspace_ / total_threads;
std::size_t remainder = total_keyspace_ % total_threads;
start_index_ = thread_index * per_thread
+ std::min(static_cast<std::size_t>(thread_index), remainder);
start_index_ =
thread_index * per_thread + std::min(static_cast<std::size_t>(thread_index), remainder);
std::size_t my_count = per_thread + (thread_index < remainder ? 1 : 0);
end_index_ = start_index_ + my_count;
current_index_ = start_index_;
@ -83,5 +80,9 @@ std::expected<std::string, AttackComplete> BruteForceAttack::next() {
return index_to_candidate(current_index_++);
}
std::size_t BruteForceAttack::total() const { return total_keyspace_; }
std::size_t BruteForceAttack::progress() const { return current_index_ - start_index_; }
std::size_t BruteForceAttack::total() const {
return total_keyspace_;
}
std::size_t BruteForceAttack::progress() const {
return current_index_ - start_index_;
}

View File

@ -12,16 +12,16 @@ Connects to:
#pragma once
#include "src/core/Concepts.hpp"
#include <cstddef>
#include <expected>
#include <string>
#include <string_view>
#include "src/core/Concepts.hpp"
class BruteForceAttack {
public:
BruteForceAttack(std::string_view charset, std::size_t max_length,
unsigned thread_index, unsigned total_threads);
BruteForceAttack(std::string_view charset, std::size_t max_length, unsigned thread_index,
unsigned total_threads);
std::expected<std::string, AttackComplete> next();
std::size_t total() const;
@ -36,6 +36,5 @@ private:
std::size_t current_index_;
std::string index_to_candidate(std::size_t index) const;
static std::size_t compute_keyspace(std::size_t charset_size,
std::size_t max_length);
static std::size_t compute_keyspace(std::size_t charset_size, std::size_t max_length);
};

View File

@ -27,9 +27,7 @@ Connects to:
#include "src/attack/DictionaryAttack.hpp"
#include <algorithm>
static std::size_t count_lines_in_range(const char* data,
std::size_t start,
std::size_t end) {
static std::size_t count_lines_in_range(const char *data, std::size_t start, std::size_t end) {
std::size_t count = 0;
for (std::size_t i = start; i < end; ++i) {
if (data[i] == '\n') {
@ -39,17 +37,15 @@ static std::size_t count_lines_in_range(const char* data,
return count;
}
static std::size_t find_next_newline(const char* data,
std::size_t pos,
std::size_t size) {
static std::size_t find_next_newline(const char *data, std::size_t pos, std::size_t size) {
while (pos < size && data[pos] != '\n') {
++pos;
}
return pos < size ? pos + 1 : size;
}
std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
std::string_view path, unsigned thread_index, unsigned total_threads) {
std::expected<DictionaryAttack, CrackError>
DictionaryAttack::create(std::string_view path, unsigned thread_index, unsigned total_threads) {
auto file = MappedFile::open(path);
if (!file.has_value()) {
return std::unexpected(file.error());
@ -66,10 +62,9 @@ std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
std::size_t lines_per_thread = total_lines / total_threads;
std::size_t remainder = total_lines % total_threads;
std::size_t my_start_line = thread_index * lines_per_thread
+ std::min(static_cast<std::size_t>(thread_index), remainder);
std::size_t my_line_count = lines_per_thread
+ (thread_index < remainder ? 1 : 0);
std::size_t my_start_line = thread_index * lines_per_thread +
std::min(static_cast<std::size_t>(thread_index), remainder);
std::size_t my_line_count = lines_per_thread + (thread_index < remainder ? 1 : 0);
std::size_t start_offset = 0;
for (std::size_t i = 0; i < my_start_line; ++i) {
@ -116,5 +111,9 @@ std::expected<std::string, AttackComplete> DictionaryAttack::next() {
return std::unexpected(AttackComplete{});
}
std::size_t DictionaryAttack::total() const { return total_words_; }
std::size_t DictionaryAttack::progress() const { return words_read_; }
std::size_t DictionaryAttack::total() const {
return total_words_;
}
std::size_t DictionaryAttack::progress() const {
return words_read_;
}

View File

@ -14,17 +14,17 @@ Connects to:
#pragma once
#include "src/core/Concepts.hpp"
#include "src/io/MappedFile.hpp"
#include <cstddef>
#include <expected>
#include <string>
#include <string_view>
#include "src/core/Concepts.hpp"
#include "src/io/MappedFile.hpp"
class DictionaryAttack {
public:
static std::expected<DictionaryAttack, CrackError> create(
std::string_view path, unsigned thread_index, unsigned total_threads);
static std::expected<DictionaryAttack, CrackError>
create(std::string_view path, unsigned thread_index, unsigned total_threads);
std::expected<std::string, AttackComplete> next();
std::size_t total() const;

View File

@ -30,9 +30,9 @@ Connects to:
RuleAttack::RuleAttack(DictionaryAttack dict, bool chain_rules)
: dict_(std::move(dict)), chain_rules_(chain_rules) {}
std::expected<RuleAttack, CrackError> RuleAttack::create(
std::string_view path, bool chain_rules,
unsigned thread_index, unsigned total_threads) {
std::expected<RuleAttack, CrackError> RuleAttack::create(std::string_view path, bool chain_rules,
unsigned thread_index,
unsigned total_threads) {
auto dict = DictionaryAttack::create(path, thread_index, total_threads);
if (!dict.has_value()) {
return std::unexpected(dict.error());
@ -77,5 +77,9 @@ std::expected<std::string, AttackComplete> RuleAttack::next() {
return std::move(mutations_[mutation_index_++]);
}
std::size_t RuleAttack::total() const { return dict_.total(); }
std::size_t RuleAttack::progress() const { return candidates_yielded_; }
std::size_t RuleAttack::total() const {
return dict_.total();
}
std::size_t RuleAttack::progress() const {
return candidates_yielded_;
}

View File

@ -17,19 +17,18 @@ Connects to:
#pragma once
#include "src/attack/DictionaryAttack.hpp"
#include "src/core/Concepts.hpp"
#include <cstddef>
#include <expected>
#include <string>
#include <string_view>
#include <vector>
#include "src/attack/DictionaryAttack.hpp"
#include "src/core/Concepts.hpp"
class RuleAttack {
public:
static std::expected<RuleAttack, CrackError> create(
std::string_view path, bool chain_rules,
unsigned thread_index, unsigned total_threads);
static std::expected<RuleAttack, CrackError>
create(std::string_view path, bool chain_rules, unsigned thread_index, unsigned total_threads);
std::expected<std::string, AttackComplete> next();
std::size_t total() const;

View File

@ -70,7 +70,7 @@ constexpr std::string_view CYAN = "\033[36m";
constexpr std::string_view BOLD = "\033[1m";
constexpr std::string_view DIM = "\033[2m";
}
} // namespace color
namespace box {
@ -85,7 +85,7 @@ constexpr std::string_view BLOCK_EMPTY = "\u2591";
constexpr std::string_view BAR_LEFT = "\u2590";
constexpr std::string_view BAR_RIGHT = "\u258C";
}
} // namespace box
namespace symbol {
@ -99,9 +99,9 @@ constexpr std::string_view TRIANGLE_UP = "\u25B2";
constexpr std::string_view STAR = "\u2726";
constexpr std::string_view DIVIDER_CHAR = "\u2501";
}
} // namespace symbol
}
} // namespace config
struct CrackConfig {
std::string target_hash;

View File

@ -50,12 +50,18 @@ enum class CrackError {
constexpr std::string_view crack_error_message(CrackError e) {
switch (e) {
case CrackError::FileNotFound: return "File not found";
case CrackError::InvalidHash: return "Invalid hash format";
case CrackError::UnsupportedAlgorithm: return "Unsupported hash algorithm";
case CrackError::OpenSSLError: return "OpenSSL internal error";
case CrackError::InvalidConfig: return "Invalid configuration";
case CrackError::Exhausted: return "All candidates exhausted";
case CrackError::FileNotFound:
return "File not found";
case CrackError::InvalidHash:
return "Invalid hash format";
case CrackError::UnsupportedAlgorithm:
return "Unsupported hash algorithm";
case CrackError::OpenSSLError:
return "OpenSSL internal error";
case CrackError::InvalidConfig:
return "Invalid configuration";
case CrackError::Exhausted:
return "All candidates exhausted";
}
return "Unknown error";
}

View File

@ -31,11 +31,6 @@ Connects to:
#pragma once
#include <chrono>
#include <cstdio>
#include <expected>
#include <string>
#include <thread>
#include "src/attack/BruteForceAttack.hpp"
#include "src/attack/DictionaryAttack.hpp"
#include "src/attack/RuleAttack.hpp"
@ -43,26 +38,32 @@ Connects to:
#include "src/core/Concepts.hpp"
#include "src/display/Progress.hpp"
#include "src/threading/ThreadPool.hpp"
#include <chrono>
#include <cstdio>
#include <expected>
#include <string>
#include <thread>
class Engine {
public:
template <Hasher H, AttackStrategy A>
static auto crack(const CrackConfig& cfg)
-> std::expected<CrackResult, CrackError>;
static auto crack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError>;
};
template <Hasher H, AttackStrategy A>
auto Engine::crack(const CrackConfig& cfg)
-> std::expected<CrackResult, CrackError> {
unsigned thread_count = cfg.thread_count > 0
? cfg.thread_count
: std::thread::hardware_concurrency();
auto Engine::crack(const CrackConfig &cfg) -> std::expected<CrackResult, CrackError> {
unsigned thread_count =
cfg.thread_count > 0 ? cfg.thread_count : std::thread::hardware_concurrency();
ThreadPool pool(thread_count);
auto attack_name = [&]() -> std::string_view {
if (cfg.bruteforce) { return "Brute Force"; }
if (cfg.use_rules) { return "Rules"; }
if (cfg.bruteforce) {
return "Brute Force";
}
if (cfg.use_rules) {
return "Rules";
}
return "Dictionary";
}();
@ -72,17 +73,20 @@ auto Engine::crack(const CrackConfig& cfg)
return probe.total();
} else if constexpr (std::same_as<A, RuleAttack>) {
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
if (!probe) { return 0; }
if (!probe) {
return 0;
}
return probe->total() * 2005;
} else {
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
if (!probe) { return 0; }
if (!probe) {
return 0;
}
return probe->total();
}
}();
Progress progress(H::name(), attack_name, thread_count,
total_estimate, pool.state().found,
Progress progress(H::name(), attack_name, thread_count, total_estimate, pool.state().found,
pool.state().tested_count);
if (Progress::is_tty()) {
@ -97,11 +101,9 @@ auto Engine::crack(const CrackConfig& cfg)
std::jthread display_thread;
if (Progress::is_tty()) {
display_thread = std::jthread([&](std::stop_token st) {
while (!st.stop_requested() &&
!pool.state().found.load(std::memory_order_relaxed)) {
while (!st.stop_requested() && !pool.state().found.load(std::memory_order_relaxed)) {
progress.update();
std::this_thread::sleep_for(
std::chrono::milliseconds(config::PROGRESS_UPDATE_MS));
std::this_thread::sleep_for(std::chrono::milliseconds(config::PROGRESS_UPDATE_MS));
}
});
}
@ -114,20 +116,23 @@ auto Engine::crack(const CrackConfig& cfg)
return std::expected<BruteForceAttack, CrackError>(
BruteForceAttack(cfg.charset, cfg.max_length, tid, total));
} else if constexpr (std::same_as<A, RuleAttack>) {
return RuleAttack::create(
cfg.wordlist_path, cfg.chain_rules, tid, total);
return RuleAttack::create(cfg.wordlist_path, cfg.chain_rules, tid, total);
} else {
return DictionaryAttack::create(cfg.wordlist_path, tid, total);
}
};
auto attack = create_attack();
if (!attack.has_value()) { return; }
if (!attack.has_value()) {
return;
}
std::size_t local_count = 0;
while (!state.found.load(std::memory_order_relaxed)) {
auto candidate = attack->next();
if (!candidate.has_value()) { break; }
if (!candidate.has_value()) {
break;
}
std::string to_hash = *candidate;
if (!cfg.salt.empty()) {
@ -161,18 +166,16 @@ auto Engine::crack(const CrackConfig& cfg)
auto end = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(end - start).count();
auto tested = pool.state().tested_count.load(std::memory_order_relaxed);
double speed = (elapsed > 0.0) ? static_cast<double>(tested) / elapsed : 0.0;
auto &state = pool.state();
if (state.found.load(std::memory_order_relaxed) && state.result.has_value()) {
CrackResult result{
.plaintext = *state.result,
double speed = (elapsed > 0.0) ? static_cast<double>(tested) / elapsed : 0.0;
CrackResult result{.plaintext = *state.result,
.hash = cfg.target_hash,
.algorithm = std::string(H::name()),
.elapsed_seconds = elapsed,
.candidates_tested = tested,
.hashes_per_second = speed
};
.hashes_per_second = speed};
progress.print_cracked(result);
return result;

View File

@ -35,13 +35,11 @@ Connects to:
#include <sys/ioctl.h>
#include <unistd.h>
Progress::Progress(std::string_view algorithm, std::string_view attack_mode,
unsigned thread_count, std::size_t total_candidates,
const std::atomic<bool>& found,
Progress::Progress(std::string_view algorithm, std::string_view attack_mode, unsigned thread_count,
std::size_t total_candidates, const std::atomic<bool> &found,
const std::atomic<std::size_t> &tested)
: algorithm_(algorithm), attack_mode_(attack_mode),
thread_count_(thread_count), total_(total_candidates),
found_(found), tested_(tested),
: algorithm_(algorithm), attack_mode_(attack_mode), thread_count_(thread_count),
total_(total_candidates), found_(found), tested_(tested),
start_time_(std::chrono::steady_clock::now()) {}
bool Progress::is_tty() {
@ -85,7 +83,9 @@ std::string Progress::render_bar(double fraction, std::size_t width) const {
}
auto filled = static_cast<std::size_t>(fraction * static_cast<double>(width));
if (filled > width) { filled = width; }
if (filled > width) {
filled = width;
}
std::string bar;
bar += config::box::BAR_LEFT;
@ -97,7 +97,9 @@ std::string Progress::render_bar(double fraction, std::size_t width) const {
}
void Progress::print_banner() const {
if (!is_tty()) { return; }
if (!is_tty()) {
return;
}
auto w = terminal_width();
auto inner_width = (w > 6) ? w - 6 : 40;
@ -111,42 +113,38 @@ void Progress::print_banner() const {
bot_border += config::box::HORIZONTAL;
}
auto line1 = std::format(" {} {} v{}",
config::APP_NAME, config::box::VERTICAL, config::VERSION);
auto line2 = std::format(" {} {} {} {} {} threads",
config::box::VERTICAL, algorithm_, config::box::VERTICAL,
attack_mode_, thread_count_);
auto line1 =
std::format(" {} {} v{}", config::APP_NAME, config::box::VERTICAL, config::VERSION);
auto line2 = std::format(" {} {} {} {} {} threads", config::box::VERTICAL, algorithm_,
config::box::VERTICAL, attack_mode_, thread_count_);
std::println("{}{}{}{}{}",
config::color::CYAN, config::box::TOP_LEFT,
top_border, config::box::TOP_RIGHT, config::color::RESET);
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
config::box::VERTICAL, line1, inner_width - 1,
config::box::VERTICAL, config::color::RESET);
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
config::box::VERTICAL, line2, inner_width - 1,
config::box::VERTICAL, config::color::RESET);
std::println("{}{}{}{}{}",
config::color::CYAN, config::box::BOTTOM_LEFT,
bot_border, config::box::BOTTOM_RIGHT, config::color::RESET);
std::println("{}{}{}{}{}", config::color::CYAN, config::box::TOP_LEFT, top_border,
config::box::TOP_RIGHT, config::color::RESET);
std::println("{}{} {:<{}}{}{}", config::color::CYAN, config::box::VERTICAL, line1,
inner_width - 1, config::box::VERTICAL, config::color::RESET);
std::println("{}{} {:<{}}{}{}", config::color::CYAN, config::box::VERTICAL, line2,
inner_width - 1, config::box::VERTICAL, config::color::RESET);
std::println("{}{}{}{}{}", config::color::CYAN, config::box::BOTTOM_LEFT, bot_border,
config::box::BOTTOM_RIGHT, config::color::RESET);
std::println("");
}
void Progress::update() {
if (!is_tty()) { return; }
if (!is_tty()) {
return;
}
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - start_time_).count();
auto tested_val = tested_.load(std::memory_order_relaxed);
double fraction = (total_ > 0)
? static_cast<double>(tested_val) / static_cast<double>(total_)
: 0.0;
if (fraction > 1.0) { fraction = 1.0; }
double fraction =
(total_ > 0) ? static_cast<double>(tested_val) / static_cast<double>(total_) : 0.0;
if (fraction > 1.0) {
fraction = 1.0;
}
double speed = (elapsed > 0.0)
? static_cast<double>(tested_val) / elapsed
: 0.0;
double speed = (elapsed > 0.0) ? static_cast<double>(tested_val) / elapsed : 0.0;
double eta = (speed > 0.0 && total_ > tested_val)
? static_cast<double>(total_ - tested_val) / speed
@ -157,19 +155,13 @@ void Progress::update() {
std::print("\033[3A");
std::println(" {}{} {:.1f}%{}",
config::color::YELLOW, render_bar(fraction, bar_width),
std::println(" {}{} {:.1f}%{}", config::color::YELLOW, render_bar(fraction, bar_width),
fraction * 100.0, config::color::RESET);
std::println(" {} {} {} {} {} {} {} ~{}{}",
config::symbol::DIAMOND, config::color::CYAN,
format_speed(speed), config::color::RESET,
config::symbol::TIMER, config::color::CYAN,
format_time(elapsed), format_time(eta),
config::color::RESET);
std::println(" {} {} {} / {} candidates{}",
config::symbol::ARROW_RIGHT, config::color::CYAN,
format_count(tested_val), format_count(total_),
config::color::RESET);
std::println(" {} {} {} {} {} {} {} ~{}{}", config::symbol::DIAMOND, config::color::CYAN,
format_speed(speed), config::color::RESET, config::symbol::TIMER,
config::color::CYAN, format_time(elapsed), format_time(eta), config::color::RESET);
std::println(" {} {} {} / {} candidates{}", config::symbol::ARROW_RIGHT, config::color::CYAN,
format_count(tested_val), format_count(total_), config::color::RESET);
}
void Progress::print_cracked(const CrackResult &result) const {
@ -180,22 +172,17 @@ void Progress::print_cracked(const CrackResult& result) const {
std::print("\033[3A\033[J");
std::println(" {}{} CRACKED {}{}{}",
config::color::GREEN, config::symbol::CHECK,
std::println(" {}{} CRACKED {}{}{}", config::color::GREEN, config::symbol::CHECK,
std::string(30, '-'), config::color::RESET, "");
std::println(" {}Password: {}{}{}",
config::color::BOLD, config::color::GREEN,
std::println(" {}Password: {}{}{}", config::color::BOLD, config::color::GREEN,
result.plaintext, config::color::RESET);
std::println(" Hash: {}", result.hash);
std::println(" Algorithm: {}", result.algorithm);
std::println(" Time: {} {} {}",
format_time(result.elapsed_seconds),
config::box::VERTICAL,
format_speed(result.hashes_per_second));
std::println(" Time: {} {} {}", format_time(result.elapsed_seconds),
config::box::VERTICAL, format_speed(result.hashes_per_second));
}
void Progress::print_exhausted(std::string_view hash,
std::string_view algorithm) const {
void Progress::print_exhausted(std::string_view hash, std::string_view algorithm) const {
if (!is_tty()) {
std::println("NOT FOUND");
return;
@ -207,11 +194,10 @@ void Progress::print_exhausted(std::string_view hash,
std::print("\033[3A\033[J");
std::println(" {}{} EXHAUSTED {}{}{}",
config::color::RED, config::symbol::CROSS,
std::println(" {}{} EXHAUSTED {}{}{}", config::color::RED, config::symbol::CROSS,
std::string(30, '-'), config::color::RESET, "");
std::println(" Hash: {}", hash);
std::println(" Algorithm: {}", algorithm);
std::println(" Tested: {} candidates in {}",
format_count(tested_val), format_time(elapsed));
std::println(" Tested: {} candidates in {}", format_count(tested_val),
format_time(elapsed));
}

View File

@ -22,9 +22,8 @@ struct CrackResult;
class Progress {
public:
Progress(std::string_view algorithm, std::string_view attack_mode,
unsigned thread_count, std::size_t total_candidates,
const std::atomic<bool>& found,
Progress(std::string_view algorithm, std::string_view attack_mode, unsigned thread_count,
std::size_t total_candidates, const std::atomic<bool> &found,
const std::atomic<std::size_t> &tested);
void print_banner() const;

View File

@ -65,33 +65,30 @@ struct SHA512Tag {
static constexpr std::size_t hex_length = 128;
};
}
} // namespace evp
inline constexpr std::array<std::array<char, 2>, 256> HEX_TABLE = [] {
std::array<std::array<char, 2>, 256> t{};
constexpr std::array<char, 17> digits = {
'0','1','2','3','4','5','6','7',
'8','9','a','b','c','d','e','f','\0'};
constexpr std::array<char, 17> digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f', '\0'};
for (std::size_t i = 0; i < 256; ++i) {
t.at(i) = {digits.at(i >> 4), digits.at(i & 0xF)};
}
return t;
}();
template <typename Tag>
class EVPHasher {
template <typename Tag> class EVPHasher {
public:
std::string hash(std::string_view input) const {
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
EVP_MD_CTX_new(), EVP_MD_CTX_free);
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(EVP_MD_CTX_new(),
EVP_MD_CTX_free);
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
unsigned int len = 0;
if (!ctx
|| !EVP_DigestInit_ex(ctx.get(), Tag::algorithm(), nullptr)
|| !EVP_DigestUpdate(ctx.get(), input.data(), input.size())
|| !EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
if (!ctx || !EVP_DigestInit_ex(ctx.get(), Tag::algorithm(), nullptr) ||
!EVP_DigestUpdate(ctx.get(), input.data(), input.size()) ||
!EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
return "";
}

View File

@ -26,9 +26,7 @@ Connects to:
std::expected<HashType, CrackError> HashDetector::detect(std::string_view hash) {
auto is_hex = [](char c) {
return (c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F');
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
};
if (!std::ranges::all_of(hash, is_hex)) {
@ -36,10 +34,15 @@ std::expected<HashType, CrackError> HashDetector::detect(std::string_view hash)
}
switch (hash.size()) {
case config::MD5_HEX_LENGTH: return HashType::MD5;
case config::SHA1_HEX_LENGTH: return HashType::SHA1;
case config::SHA256_HEX_LENGTH: return HashType::SHA256;
case config::SHA512_HEX_LENGTH: return HashType::SHA512;
default: return std::unexpected(CrackError::InvalidHash);
case config::MD5_HEX_LENGTH:
return HashType::MD5;
case config::SHA1_HEX_LENGTH:
return HashType::SHA1;
case config::SHA256_HEX_LENGTH:
return HashType::SHA256;
case config::SHA512_HEX_LENGTH:
return HashType::SHA512;
default:
return std::unexpected(CrackError::InvalidHash);
}
}

View File

@ -12,9 +12,9 @@ Connects to:
#pragma once
#include "src/core/Concepts.hpp"
#include <expected>
#include <string_view>
#include "src/core/Concepts.hpp"
enum class HashType { MD5, SHA1, SHA256, SHA512 };

View File

@ -47,8 +47,8 @@ std::expected<MappedFile, CrackError> MappedFile::open(std::string_view path) {
return std::unexpected(CrackError::InvalidConfig);
}
auto* mapped = static_cast<const char*>(
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
auto *mapped =
static_cast<const char *>(mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
if (mapped == MAP_FAILED) {
::close(fd);

View File

@ -12,10 +12,10 @@ Connects to:
#pragma once
#include "src/core/Concepts.hpp"
#include <cstddef>
#include <expected>
#include <string_view>
#include "src/core/Concepts.hpp"
class MappedFile {
public:

View File

@ -39,13 +39,13 @@ Connects to:
#include <ranges>
#include <utility>
static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {{
{'a', '@'}, {'e', '3'}, {'i', '1'},
{'o', '0'}, {'s', '$'}, {'t', '7'}
}};
static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {
{{'a', '@'}, {'e', '3'}, {'i', '1'}, {'o', '0'}, {'s', '$'}, {'t', '7'}}};
std::generator<std::string> RuleSet::capitalize_first(std::string_view word) {
if (word.empty()) { co_return; }
if (word.empty()) {
co_return;
}
std::string result(word);
result[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(result[0])));
co_yield std::move(result);
@ -53,9 +53,8 @@ std::generator<std::string> RuleSet::capitalize_first(std::string_view word) {
std::generator<std::string> RuleSet::uppercase_all(std::string_view word) {
std::string result(word);
std::ranges::transform(result, result.begin(), [](unsigned char c) {
return static_cast<char>(std::toupper(c));
});
std::ranges::transform(result, result.begin(),
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
co_yield std::move(result);
}
@ -64,7 +63,10 @@ std::generator<std::string> RuleSet::leet_speak(std::string_view word) {
for (auto &c : result) {
auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (auto [from, to] : LEET_MAP) {
if (lower == from) { c = to; break; }
if (lower == from) {
c = to;
break;
}
}
}
co_yield std::move(result);
@ -92,7 +94,9 @@ std::generator<std::string> RuleSet::reverse(std::string_view word) {
std::generator<std::string> RuleSet::toggle_case(std::string_view word) {
std::string result(word);
std::ranges::transform(result, result.begin(), [](unsigned char c) {
if (std::islower(c)) { return static_cast<char>(std::toupper(c)); }
if (std::islower(c)) {
return static_cast<char>(std::toupper(c));
}
return static_cast<char>(std::tolower(c));
});
co_yield std::move(result);

View File

@ -27,18 +27,17 @@ void SharedState::set_result(std::string plaintext) {
}
ThreadPool::ThreadPool(unsigned thread_count)
: thread_count_(thread_count > 0 ? thread_count
: std::thread::hardware_concurrency()) {}
: thread_count_(thread_count > 0 ? thread_count : std::thread::hardware_concurrency()) {}
void ThreadPool::run(WorkFn work) {
std::vector<std::jthread> threads;
threads.reserve(thread_count_);
for (unsigned i = 0; i < thread_count_; ++i) {
threads.emplace_back([this, &work, i] {
work(i, thread_count_, state_);
});
threads.emplace_back([this, &work, i] { work(i, thread_count_, state_); });
}
}
SharedState& ThreadPool::state() { return state_; }
SharedState &ThreadPool::state() {
return state_;
}

View File

@ -13,8 +13,8 @@ Connects to:
attack/BruteForceAttack.hpp - BruteForceAttack tested
*/
#include <gtest/gtest.h>
#include "src/attack/BruteForceAttack.hpp"
#include <gtest/gtest.h>
#include <set>
#include <vector>
@ -42,8 +42,12 @@ TEST(BruteForceAttackTest, PartitionsKeyspace) {
BruteForceAttack p0("ab", 2, 0, 2);
BruteForceAttack p1("ab", 2, 1, 2);
std::set<std::string> all;
while (auto c = p0.next()) { all.insert(*c); }
while (auto c = p1.next()) { all.insert(*c); }
while (auto c = p0.next()) {
all.insert(*c);
}
while (auto c = p1.next()) {
all.insert(*c);
}
EXPECT_EQ(all.size(), 6);
}

View File

@ -14,8 +14,8 @@ Connects to:
tests/data/small_wordlist.txt - fixture wordlist
*/
#include <gtest/gtest.h>
#include "src/attack/DictionaryAttack.hpp"
#include <gtest/gtest.h>
#include <vector>
TEST(DictionaryAttackTest, ReadsAllWords) {
@ -38,8 +38,12 @@ TEST(DictionaryAttackTest, PartitionsAcrossThreads) {
ASSERT_TRUE(p1.has_value());
std::size_t count0 = 0, count1 = 0;
while (p0->next()) { ++count0; }
while (p1->next()) { ++count1; }
while (p0->next()) {
++count0;
}
while (p1->next()) {
++count1;
}
EXPECT_EQ(count0 + count1, 10);
}

View File

@ -16,14 +16,13 @@ Connects to:
tests/data/small_wordlist.txt - fixture wordlist
*/
#include <gtest/gtest.h>
#include "src/core/Engine.hpp"
#include "src/hash/SHA256Hasher.hpp"
#include <gtest/gtest.h>
TEST(EngineTest, CracksSHA256WithDictionary) {
CrackConfig cfg;
cfg.target_hash =
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
cfg.target_hash = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
cfg.wordlist_path = "tests/data/small_wordlist.txt";
cfg.thread_count = 2;

View File

@ -13,8 +13,8 @@ Connects to:
core/Concepts.hpp - CrackError enum for error assertions
*/
#include <gtest/gtest.h>
#include "src/hash/HashDetector.hpp"
#include <gtest/gtest.h>
TEST(HashDetectorTest, DetectsMD5) {
auto result = HashDetector::detect("d41d8cd98f00b204e9800998ecf8427e");
@ -29,8 +29,8 @@ TEST(HashDetectorTest, DetectsSHA1) {
}
TEST(HashDetectorTest, DetectsSHA256) {
auto result = HashDetector::detect(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
auto result =
HashDetector::detect("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(*result, HashType::SHA256);
}

View File

@ -16,16 +16,15 @@ Connects to:
hash/EVPHasher.hpp - underlying implementation
*/
#include <gtest/gtest.h>
#include "src/hash/MD5Hasher.hpp"
#include "src/hash/SHA1Hasher.hpp"
#include "src/hash/SHA256Hasher.hpp"
#include "src/hash/SHA512Hasher.hpp"
#include <gtest/gtest.h>
TEST(SHA256HasherTest, KnownVectors) {
SHA256Hasher hasher;
EXPECT_EQ(hasher.hash(""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
EXPECT_EQ(hasher.hash(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
EXPECT_EQ(hasher.hash("password"),
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8");
EXPECT_EQ(hasher.hash("hello"),
@ -68,8 +67,7 @@ TEST(SHA1HasherTest, StaticProperties) {
TEST(SHA512HasherTest, KnownVectors) {
SHA512Hasher hasher;
EXPECT_EQ(hasher.hash(""),
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"
EXPECT_EQ(hasher.hash(""), "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce"
"47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");
EXPECT_EQ(hasher.hash("password"),
"b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb9"

View File

@ -17,18 +17,15 @@ Connects to:
tests/data/small_wordlist.txt - fixture wordlist
*/
#include <gtest/gtest.h>
#include "src/attack/RuleAttack.hpp"
#include "src/rules/RuleSet.hpp"
#include <algorithm>
#include <gtest/gtest.h>
#include <ranges>
#include <vector>
static std::vector<std::string> collect(std::generator<std::string> gen) {
std::vector<std::string> out;
for (auto&& s : gen) {
out.push_back(std::move(s));
}
return out;
return std::ranges::to<std::vector<std::string>>(std::move(gen));
}
TEST(RuleSetTest, CapitalizeFirst) {
@ -103,10 +100,14 @@ TEST(RuleAttackTest, ChainRulesProducesMoreCandidates) {
ASSERT_TRUE(with_chain.has_value());
std::size_t count_without = 0;
while (without->next()) { ++count_without; }
while (without->next()) {
++count_without;
}
std::size_t count_with = 0;
while (with_chain->next()) { ++count_with; }
while (with_chain->next()) {
++count_with;
}
EXPECT_GT(count_with, count_without);
}

View File

@ -0,0 +1,10 @@
# ©AngelaMos | 2026
# .shellcheckrc
#
# Disabled rules document false-positive systematic patterns in this project
# SC2034: CURRENT_TEST / SYSROOT pattern read by test framework via introspection
# SC1090: dynamic source paths in test runner (intentional discovery)
# SC1091: helper-file sources resolved at runtime, not visible to shellcheck
disable=SC2034
disable=SC1090
disable=SC1091

View File

@ -333,6 +333,7 @@ check_4_2_3() {
fi
local file_mode
# shellcheck disable=SC2016 # literal $FileCreateMode and $2 are regex/awk syntax
file_mode=$(grep -E '^\s*\$FileCreateMode' "$rsyslog_conf" | tail -1 | awk '{print $2}') || true
if [[ -z "$file_mode" ]]; then
@ -369,6 +370,7 @@ check_4_2_4() {
evidence="Found ${rule_count} logging rule(s) in rsyslog.conf"
else
local include_count
# shellcheck disable=SC2016 # literal $IncludeConfig is regex syntax, not shell
include_count=$(grep -cE '^\s*\$IncludeConfig|^\s*include\(' "$rsyslog_conf") || true
if [[ "$include_count" -gt 0 ]]; then

View File

@ -110,6 +110,7 @@ emit_json_report() {
local status="${RESULT_STATUS[$id]}"
local evidence="${RESULT_EVIDENCE[$id]:-}"
local title="${CTRL_TITLE[$id]}"
# shellcheck disable=SC2153 # CTRL_SECTION is a populated global associative array
local ctrl_section="${CTRL_SECTION[$id]}"
local level="${CTRL_LEVEL[$id]}"
local scored="${CTRL_SCORED[$id]}"

View File

@ -25,7 +25,7 @@ fail() { echo -e "${RED}[✖]${RESET} $1" >&2; exit "$EXIT_FAIL"; }
progress() {
[[ "$QUIET" == "true" ]] && return
printf "\r${DIM}[%s] Checking: %s${RESET}%s" "$1" "$2" "$(printf '%40s')" >&2
printf "\r${DIM}[%s] Checking: %s${RESET}%s" "$1" "$2" "$(printf '%40s' '')" >&2
}
clear_progress() {
@ -120,6 +120,7 @@ service_is_active() {
}
package_is_installed() {
# shellcheck disable=SC2016 # ${Status} is dpkg-query template syntax, not shell expansion
if run_cmd dpkg-query -W -f='${Status}' "$1" | grep -q "install ok installed"; then
return 0
fi

View File

@ -138,11 +138,11 @@ test_baseline_missing_file() {
CURRENT_TEST="test_baseline_missing_file"
setup_test "${PROJECT_DIR}/testdata/fixtures"
local result
result=$(load_baseline "/tmp/nonexistent_baseline_12345.json" 2>&1) || true
local result rc
result=$(load_baseline "/tmp/nonexistent_baseline_12345.json" 2>&1) && rc=0 || rc=$?
((TEST_TOTAL++)) || true
if [[ $? -eq 0 ]] || echo "$result" | grep -q "not found"; then
if (( rc == 0 )) || echo "$result" | grep -q "not found"; then
((TEST_PASS++)) || true
else
((TEST_FAIL++)) || true

View File

@ -21,8 +21,8 @@ class IP_class {
std::string dst;
public:
std::string get_source();
std::string get_dest();
const std::string &get_source() const;
const std::string &get_dest() const;
// getters
/*virtual std::string get_source() = 0;
virtual std::string get_dest() = 0;*/

View File

@ -39,8 +39,8 @@ int main(int argc, char **argv) {
/* get a filter, use vector for multiple */
std::vector<filter> filters;
if (parser.vm.contains("filter")) {
auto &f = parser.vm["filter"].as<std::vector<std::string>>();
for (auto &x : f) {
const auto &f = parser.vm["filter"].as<std::vector<std::string>>();
for (const auto &x : f) {
filters.push_back(parse(x));
filterString += x + " ";
}
@ -82,13 +82,10 @@ int main(int argc, char **argv) {
/* our class for UI */
View view;
std::mutex render_mtx;
std::mutex event_mtx;
ftxui::Element current_render = isOffline
? view.render(stats.get_snapshot(), interface, filterString, true, timer.load())
: ftxui::text("Starting capture...");
std::mutex screen_mtx;
auto component = ftxui::Renderer([&] {
std::lock_guard<std::mutex> lock(render_mtx);
return current_render;

View File

@ -112,7 +112,7 @@ void PcapCapture::stop() {
/* print all available interfaces */
void PcapCapture::print_interfaces() {
int i = 0;
for (pcap_if_t *dev = interfaces; dev != nullptr; dev = dev->next) {
for (const pcap_if_t *dev = interfaces; dev != nullptr; dev = dev->next) {
printf("%d. %s ", ++i, dev->name);
if (dev->description != nullptr) {
printf("(%s)\n", dev->description);

View File

@ -11,13 +11,12 @@
uint16_t IP_class::get_payload_len() const { return payload_len; }
TransportProtocol IP_class::get_protocol() const { return protocol; }
std::string IP_class::get_source() { return src; }
std::string IP_class::get_dest() { return dst; }
const std::string &IP_class::get_source() const { return src; }
const std::string &IP_class::get_dest() const { return dst; }
/*** Ipv4 ***/
IPv4::IPv4(const u_char *data)
: ip_hdr(reinterpret_cast<const ip *>(data)),
ip_hdr_len(static_cast<int>(ip_hdr->ip_hl) * 4) {
: ip_hdr(reinterpret_cast<const ip *>(data)), ip_hdr_len(static_cast<int>(ip_hdr->ip_hl) * 4) {
std::array<char, INET_ADDRSTRLEN> src_buf{};
inet_ntop(AF_INET, &ip_hdr->ip_src, src_buf.data(), sizeof(src_buf));
src = src_buf.data();
@ -84,8 +83,7 @@ uint16_t IPv4::get_dest_port() { return dest_port; }
/*** Ipv6 ***/
IPv6::IPv6(const u_char *data)
: ip_hdr(reinterpret_cast<const ip6_hdr *>(data)),
ptr(reinterpret_cast<const uint8_t *>(ip_hdr + 1)) {
: ip_hdr(reinterpret_cast<const ip6_hdr *>(data)), ptr(reinterpret_cast<const uint8_t *>(ip_hdr + 1)) {
uint8_t hdr = ip_hdr->ip6_nxt;
std::array<char, INET6_ADDRSTRLEN> src{};
inet_ntop(AF_INET6, &ip_hdr->ip6_src, src.data(), sizeof(src));

View File

@ -15,7 +15,7 @@ ApplicationProtocol Packet::get_application_protocol() {
return ApplicationProtocol::DNS;
}
if (transport_protocol == TransportProtocol::TCP && payload_len >= 3) {
if (transport_protocol == TransportProtocol::TCP) {
if (payload_ptr[0] == 0x16 && payload_ptr[1] == 0x03) {
return ApplicationProtocol::HTTPS;
}

View File

@ -2,7 +2,7 @@
#include "ftxui/dom/table.hpp"
#include <fstream>
Stats::Stats() { last_tick = std::chrono::steady_clock::now(); }
Stats::Stats() : last_tick(std::chrono::steady_clock::now()) {}
/**
* @brief Aggregates a newly captured packet.
*
@ -96,7 +96,7 @@ void Stats::update_transport_stats() {
snapshot.transport_rows.push_back({"Proto", "Packets", "Bytes", "%"});
std::vector<std::pair<TransportProtocol, protocolStats>> tps(transport_map.begin(), transport_map.end());
std::sort(tps.begin(), tps.end(), [](auto &a, auto &b) { return a.second.packets > b.second.packets; });
std::sort(tps.begin(), tps.end(), [](const auto &a, const auto &b) { return a.second.packets > b.second.packets; });
for (const auto &[proto, stats] : tps) {
double percent = snapshot.total_b ? stats.bytes * 100.0 / snapshot.total_b : 0.0;
@ -116,7 +116,8 @@ void Stats::update_application_stats() {
std::lock_guard<std::mutex> lock(mtx);
std::vector<std::pair<ApplicationProtocol, protocolStats>> apps(application_map.begin(), application_map.end());
std::sort(apps.begin(), apps.end(), [](auto &a, auto &b) { return a.second.packets > b.second.packets; });
std::sort(apps.begin(), apps.end(),
[](const auto &a, const auto &b) { return a.second.packets > b.second.packets; });
snapshot.app_rows.clear();
snapshot.app_rows.push_back({"Proto", "Packets", "Bytes (MB)", "%"});
@ -144,7 +145,8 @@ void Stats::update_ip_stats(size_t limit) {
snapshot.rows.push_back({"IP Address", "Packets TX", "Packets RX"});
std::vector<std::pair<std::string, IPStats>> ips(ip_map.begin(), ip_map.end());
std::sort(ips.begin(), ips.end(), [](auto &a, auto &b) { return a.second.packets_sent > b.second.packets_sent; });
std::sort(ips.begin(), ips.end(),
[](const auto &a, const auto &b) { return a.second.packets_sent > b.second.packets_sent; });
size_t count = 0;
for (const auto &[ip, s] : ips) {
@ -166,7 +168,7 @@ void Stats::update_ip_stats(size_t limit) {
void Stats::update_pairs(size_t limit) {
std::lock_guard<std::mutex> lock(mtx);
std::vector<std::pair<std::pair<std::string, std::string>, protocolStats>> vec(pairs.begin(), pairs.end());
std::sort(vec.begin(), vec.end(), [](auto &a, auto &b) { return a.second.bytes > b.second.bytes; });
std::sort(vec.begin(), vec.end(), [](const auto &a, const auto &b) { return a.second.bytes > b.second.bytes; });
snapshot.pairs_rows.clear();
snapshot.pairs_rows.push_back({"Source", "Destination", "bytes received", "%"});

View File

@ -5,13 +5,14 @@ namespace po = boost::program_options;
int main(int argc, char* argv[]) {
po::options_description desc("Allowed options");
desc.add_options()
("help,h", "produce help message")
("dname,i", po::value<std::string>()->default_value("127.0.0.1"), "set domain name or IP address")
("ports,p", po::value<std::string>()->default_value("1-1024"), "set a port range from 1 to n")
("threads,t", po::value<int>()->default_value(100), "max concurrent threads")
("expiry_time,e", po::value<uint8_t>()->default_value(2)->value_name("sec"), "timeout in seconds")
("verbose,v", "verbose output");
desc.add_options()("help,h", "produce help message")(
"dname,i", po::value<std::string>()->default_value("127.0.0.1"),
"set domain name or IP address")("ports,p",
po::value<std::string>()->default_value("1-1024"),
"set a port range from 1 to n")(
"threads,t", po::value<int>()->default_value(100), "max concurrent threads")(
"expiry_time,e", po::value<uint8_t>()->default_value(2)->value_name("sec"),
"timeout in seconds")("verbose,v", "verbose output");
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);

View File

@ -1,26 +1,10 @@
#include "PortScanner.hpp"
const std::unordered_map<uint16_t, std::string> PortScanner::basicPorts{
{21, "FTP"},
{22, "SSH"},
{23, "TelNet"},
{25, "SMTP"},
{53, "DNS"},
{67, "DHCP server"},
{68, "DHCP client"},
{80, "HTTP"},
{110, "POP3"},
{143, "IMAP"},
{161, "SNMP"},
{443, "HTTPS"},
{445, "SMB"},
{465, "SMTPS"},
{993, "IMAPS"},
{1080, "SOCKS"},
{1521, "ORACLE DB"},
{3306, "MySQL"},
{3389, "RDP"},
{5432, "PostgreSQL"},
{21, "FTP"}, {22, "SSH"}, {23, "TelNet"}, {25, "SMTP"}, {53, "DNS"},
{67, "DHCP server"}, {68, "DHCP client"}, {80, "HTTP"}, {110, "POP3"}, {143, "IMAP"},
{161, "SNMP"}, {443, "HTTPS"}, {445, "SMB"}, {465, "SMTPS"}, {993, "IMAPS"},
{1080, "SOCKS"}, {1521, "ORACLE DB"}, {3306, "MySQL"}, {3389, "RDP"}, {5432, "PostgreSQL"},
{6379, "Redis"},
};
@ -46,19 +30,18 @@ void PortScanner::parse_port(std::string& port) {
++it;
}
int start = std::stoi(s);
int end = std::stoi(e);
//check a valid bounds
if (start == 0 || end > MAX_PORT || start > end) {
int start_port = std::stoi(s);
int end_port = std::stoi(e);
if (start_port == 0 || end_port > MAX_PORT || start_port > end_port) {
startPort = 1;
endPort = MAX_PORT;
}
else {
startPort = static_cast<uint16_t>(start);
endPort = static_cast<uint16_t>(end);
} else {
startPort = static_cast<uint16_t>(start_port);
endPort = static_cast<uint16_t>(end_port);
}
}
PortScanner::PortScanner(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time) {
PortScanner::PortScanner(std::string& domainName, std::string& port, int max_threads,
std::uint8_t expiry_time) {
this->domainName = std::move(domainName);
this->MAX_THREADS = max_threads;
this->expiry_time = expiry_time;
@ -68,7 +51,6 @@ PortScanner::PortScanner(std::string& domainName, std::string& port, int max_thr
endpoint = *result.begin();
setup_queue();
}
void PortScanner::setup_queue() {
@ -78,18 +60,15 @@ void PortScanner::setup_queue() {
}
}
void PortScanner::set_options(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time) {
void PortScanner::set_options(std::string& domainName, std::string& port, int max_threads,
std::uint8_t expiry_time) {
this->domainName = std::move(domainName);
this->MAX_THREADS = max_threads;
this->expiry_time = expiry_time;
parse_port(port);
auto result = resolver.resolve(this->domainName, "");
endpoint = *result.begin();
}
void PortScanner::set_max_port(std::uint16_t port) {
@ -101,7 +80,6 @@ void PortScanner::set_max_threads(int value) {
void PortScanner::set_ip_address(std::string ip) {
domainName = std::move(ip);
}
void PortScanner::set_expiry_time(std::uint8_t value) {
@ -111,10 +89,7 @@ void PortScanner::set_expiry_time(std::uint8_t value) {
void PortScanner::start() {
setup_queue();
for (int i = 0; i < MAX_THREADS; i++) {
boost::asio::post(strand, [this]() {
scan();
});
boost::asio::post(strand, [this]() { scan(); });
}
}
@ -128,7 +103,8 @@ void PortScanner::run() {
}
void PortScanner::scan() {
if (q.empty() || cnt >= MAX_THREADS) return;
if (q.empty() || cnt >= MAX_THREADS)
return;
uint16_t port = q.front();
q.pop();
@ -138,11 +114,12 @@ void PortScanner::scan() {
auto timer = std::make_shared<boost::asio::steady_timer>(io);
auto complete = std::make_shared<bool>(false);
tcp::endpoint endpoint(this->endpoint.address(), port);
tcp::endpoint target_endpoint(this->endpoint.address(), port);
timer->expires_after(std::chrono::seconds(expiry_time));
timer->async_wait(boost::asio::bind_executor(strand, [this, complete, socket, port](boost::system::error_code ec) {
timer->async_wait(boost::asio::bind_executor(
strand, [this, complete, socket, port](boost::system::error_code ec) {
if (!ec && !*complete) {
*complete = true;
socket->close();
@ -151,11 +128,13 @@ void PortScanner::scan() {
--cnt;
scan();
}
}));
socket->async_connect(endpoint, boost::asio::bind_executor(strand, [this,socket, timer, port, complete](boost::system::error_code ec) {
if (*complete) return;
socket->async_connect(target_endpoint,
boost::asio::bind_executor(strand, [this, socket, timer, port, complete](
boost::system::error_code ec) {
if (*complete)
return;
*complete = true;
timer->cancel();
@ -169,25 +148,27 @@ void PortScanner::scan() {
if (!ec) {
auto buf = std::make_shared<std::array<char, 128>>();
socket->async_read_some(boost::asio::buffer(*buf),boost::asio::bind_executor(strand,
[this, port, buf, banner, service](boost::system::error_code ec, std::size_t n) {
socket->async_read_some(
boost::asio::buffer(*buf),
boost::asio::bind_executor(
strand, [this, port, buf, banner, service](
boost::system::error_code ec, std::size_t n) {
if (!ec && n > 0) {
banner->assign(buf->data(), n);
}
printf("%i\t%sOPEN%s\t%s\t%s\n", port, GREEN,RESET, service.c_str(), banner->c_str());
printf("%i\t%sOPEN%s\t%s\t%s\n", port, GREEN, RESET,
service.c_str(), banner->c_str());
++open_ports;
--cnt;
scan();
}));
}
else{
printf("%i\t%sCLOSED%s\t%s\t%s\n", port, RED, RESET, service.c_str(), banner->c_str());
} else {
printf("%i\t%sCLOSED%s\t%s\t%s\n", port, RED, RESET,
service.c_str(), banner->c_str());
++closed_ports;
--cnt;
scan();
}
}));
}

View File

@ -1,14 +1,14 @@
#ifndef PORTSCANNER_HPP
#define PORTSCANNER_HPP
#include <boost/asio.hpp>
#include <string>
#include <queue>
#include <unordered_map>
#include <iostream>
#include <array>
#include <memory>
#include <boost/asio.hpp>
#include <cstdint>
#include <iostream>
#include <memory>
#include <queue>
#include <stdio.h>
#include <string>
#include <unordered_map>
#define RED "\033[31m"
#define GREEN "\033[32m"
@ -38,14 +38,17 @@ private:
void scan();
void setup_queue();
void parse_port(std::string& port);
public:
PortScanner(std::string& ip_address, std::string& port, int max_threads, std::uint8_t expiry_time);
PortScanner(){}
~PortScanner() {
public:
PortScanner(std::string& ip_address, std::string& port, int max_threads,
std::uint8_t expiry_time);
PortScanner() {
}
~PortScanner() {
}
void set_options(std::string& domainName, std::string& port, int max_threads, std::uint8_t expiry_time);
void set_options(std::string& domainName, std::string& port, int max_threads,
std::uint8_t expiry_time);
void set_max_port(std::uint16_t port);
void set_max_threads(int value);
void set_ip_address(std::string ip);

View File

@ -61,12 +61,7 @@ pub struct AnalysisContext {
}
impl AnalysisContext {
pub fn new(
source: BinarySource,
sha256: String,
file_name: String,
file_size: u64,
) -> Self {
pub fn new(source: BinarySource, sha256: String, file_name: String, file_size: u64) -> Self {
Self {
source,
sha256,

View File

@ -25,10 +25,7 @@ pub enum EngineError {
UnsupportedArchitecture { arch: String },
#[error("pass '{pass}' missing dependency: {dependency}")]
MissingDependency {
pass: String,
dependency: String,
},
MissingDependency { pass: String, dependency: String },
#[error("pass '{pass}' failed")]
PassFailed {

View File

@ -25,31 +25,23 @@
// types.rs - Architecture, BinaryFormat, Endianness,
// SectionPermissions
use goblin::elf::Elf;
use goblin::elf::dynamic::DT_BIND_NOW;
use goblin::elf::header::{
EM_386, EM_AARCH64, EM_ARM, EM_X86_64, ET_CORE, ET_DYN,
ET_EXEC, ET_REL,
EM_386, EM_AARCH64, EM_ARM, EM_X86_64, ET_CORE, ET_DYN, ET_EXEC, ET_REL,
};
use goblin::elf::program_header::{
PF_R, PF_W, PF_X, PT_DYNAMIC, PT_GNU_EH_FRAME,
PT_GNU_RELRO, PT_GNU_STACK, PT_INTERP, PT_LOAD, PT_NOTE,
PT_NULL, PT_PHDR,
};
use goblin::elf::section_header::{
SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS,
SHT_SYMTAB,
PF_R, PF_W, PF_X, PT_DYNAMIC, PT_GNU_EH_FRAME, PT_GNU_RELRO, PT_GNU_STACK, PT_INTERP, PT_LOAD,
PT_NOTE, PT_NULL, PT_PHDR,
};
use goblin::elf::section_header::{SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS, SHT_SYMTAB};
use goblin::elf::sym::STT_FUNC;
use goblin::elf::Elf;
use super::{
detect_common_anomalies, compute_section_hash, ElfInfo,
FormatResult, SectionInfo, SegmentInfo,
ElfInfo, FormatResult, SectionInfo, SegmentInfo, compute_section_hash, detect_common_anomalies,
};
use crate::error::EngineError;
use crate::types::{
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
const EI_OSABI: usize = 7;
const ELFOSABI_NONE: u8 = 0;
@ -65,12 +57,8 @@ const ELFOSABI_STANDALONE: u8 = 255;
const DT_FLAGS: u64 = 30;
const DF_BIND_NOW: u64 = 0x8;
pub fn parse_elf(
elf: &Elf,
data: &[u8],
) -> Result<FormatResult, EngineError> {
let architecture =
map_architecture(elf.header.e_machine);
pub fn parse_elf(elf: &Elf, data: &[u8]) -> Result<FormatResult, EngineError> {
let architecture = map_architecture(elf.header.e_machine);
let bits = if elf.is_64 { 64 } else { 32 };
let endianness = if elf.little_endian {
Endianness::Little
@ -85,32 +73,20 @@ pub fn parse_elf(
.any(|sh| sh.sh_type == SHT_SYMTAB);
let is_stripped = !has_symtab;
let has_interp = elf
.program_headers
.iter()
.any(|ph| ph.p_type == PT_INTERP);
let is_pie =
elf.header.e_type == ET_DYN && has_interp;
let has_interp = elf.program_headers.iter().any(|ph| ph.p_type == PT_INTERP);
let is_pie = elf.header.e_type == ET_DYN && has_interp;
let has_debug_info =
elf.section_headers.iter().any(|sh| {
let has_debug_info = elf.section_headers.iter().any(|sh| {
elf.shdr_strtab
.get_at(sh.sh_name)
.is_some_and(|name| {
name.starts_with(".debug_")
})
.is_some_and(|name| name.starts_with(".debug_"))
});
let sections = build_sections(elf, data);
let segments = build_segments(elf);
let anomalies = detect_common_anomalies(
&sections,
entry_point,
is_stripped,
);
let anomalies = detect_common_anomalies(&sections, entry_point, is_stripped);
let elf_info = build_elf_info(elf);
let function_hints =
collect_function_hints(elf, entry_point);
let function_hints = collect_function_hints(elf, entry_point);
Ok(FormatResult {
format: BinaryFormat::Elf,
@ -137,11 +113,7 @@ fn map_architecture(machine: u16) -> Architecture {
EM_X86_64 => Architecture::X86_64,
EM_ARM => Architecture::Arm,
EM_AARCH64 => Architecture::Aarch64,
other => {
Architecture::Other(format!(
"elf-machine-{other}"
))
}
other => Architecture::Other(format!("elf-machine-{other}")),
}
}
@ -188,10 +160,7 @@ fn segment_type_name(p_type: u32) -> Option<String> {
)
}
fn build_sections(
elf: &Elf,
data: &[u8],
) -> Vec<SectionInfo> {
fn build_sections(elf: &Elf, data: &[u8]) -> Vec<SectionInfo> {
elf.section_headers
.iter()
.skip(1)
@ -203,32 +172,16 @@ fn build_sections(
.to_string();
let is_nobits = shdr.sh_type == SHT_NOBITS;
let raw_offset = if is_nobits {
0
} else {
shdr.sh_offset
};
let raw_size = if is_nobits {
0
} else {
shdr.sh_size
};
let raw_offset = if is_nobits { 0 } else { shdr.sh_offset };
let raw_size = if is_nobits { 0 } else { shdr.sh_size };
let permissions = SectionPermissions {
read: (shdr.sh_flags
& u64::from(SHF_ALLOC))
!= 0,
write: (shdr.sh_flags
& u64::from(SHF_WRITE))
!= 0,
execute: (shdr.sh_flags
& u64::from(SHF_EXECINSTR))
!= 0,
read: (shdr.sh_flags & u64::from(SHF_ALLOC)) != 0,
write: (shdr.sh_flags & u64::from(SHF_WRITE)) != 0,
execute: (shdr.sh_flags & u64::from(SHF_EXECINSTR)) != 0,
};
let sha256 = compute_section_hash(
data, raw_offset, raw_size,
);
let sha256 = compute_section_hash(data, raw_offset, raw_size);
SectionInfo {
name,
@ -267,11 +220,9 @@ fn build_segments(elf: &Elf) -> Vec<SegmentInfo> {
}
fn build_elf_info(elf: &Elf) -> ElfInfo {
let os_abi =
os_abi_name(elf.header.e_ident[EI_OSABI]);
let os_abi = os_abi_name(elf.header.e_ident[EI_OSABI]);
let elf_type = elf_type_name(elf.header.e_type);
let interpreter =
elf.interpreter.map(|s| s.to_string());
let interpreter = elf.interpreter.map(|s| s.to_string());
let gnu_relro = elf
.program_headers
@ -287,23 +238,17 @@ fn build_elf_info(elf: &Elf) -> ElfInfo {
let mut bind_now = false;
if let Some(dynamic) = &elf.dynamic {
for dyn_entry in &dynamic.dyns {
let tag = dyn_entry.d_tag as u64;
let tag = dyn_entry.d_tag;
if tag == DT_BIND_NOW {
bind_now = true;
}
if tag == DT_FLAGS
&& (dyn_entry.d_val & DF_BIND_NOW) != 0
{
if tag == DT_FLAGS && (dyn_entry.d_val & DF_BIND_NOW) != 0 {
bind_now = true;
}
}
}
let needed_libraries = elf
.libraries
.iter()
.map(|s| s.to_string())
.collect();
let needed_libraries = elf.libraries.iter().map(|s| s.to_string()).collect();
ElfInfo {
os_abi,
@ -316,19 +261,12 @@ fn build_elf_info(elf: &Elf) -> ElfInfo {
}
}
fn collect_function_hints(
elf: &Elf,
entry_point: u64,
) -> Vec<u64> {
fn collect_function_hints(elf: &Elf, entry_point: u64) -> Vec<u64> {
let mut hints: Vec<u64> = elf
.syms
.iter()
.chain(elf.dynsyms.iter())
.filter(|sym| {
sym.st_type() == STT_FUNC
&& sym.st_value != 0
&& sym.st_value != entry_point
})
.filter(|sym| sym.st_type() == STT_FUNC && sym.st_value != 0 && sym.st_value != entry_point)
.map(|sym| sym.st_value)
.collect();
hints.sort_unstable();

View File

@ -26,21 +26,16 @@
// types.rs - Architecture, BinaryFormat, Endianness,
// SectionPermissions
use goblin::mach::cputype::{
CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86,
CPU_TYPE_X86_64,
};
use goblin::mach::cputype::{CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86, CPU_TYPE_X86_64};
use goblin::mach::load_command::CommandVariant;
use goblin::mach::{Mach, MachO};
use super::{
compute_section_hash, detect_common_anomalies,
FormatResult, MachOInfo, SectionInfo, SegmentInfo,
FormatResult, MachOInfo, SectionInfo, SegmentInfo, compute_section_hash,
detect_common_anomalies,
};
use crate::error::EngineError;
use crate::types::{
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
const MH_OBJECT: u32 = 1;
const MH_EXECUTE: u32 = 2;
@ -53,34 +48,23 @@ const VM_PROT_READ: u32 = 0x01;
const VM_PROT_WRITE: u32 = 0x02;
const VM_PROT_EXECUTE: u32 = 0x04;
pub fn parse_macho(
mach: &Mach,
data: &[u8],
) -> Result<FormatResult, EngineError> {
pub fn parse_macho(mach: &Mach, data: &[u8]) -> Result<FormatResult, EngineError> {
match mach {
Mach::Binary(macho) => {
parse_single_macho(macho, data, false)
}
Mach::Binary(macho) => parse_single_macho(macho, data, false),
Mach::Fat(fat) => {
for arch in fat.iter_arches() {
let arch = arch.map_err(|e| {
EngineError::InvalidBinary {
let arch = arch.map_err(|e| EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
let offset = arch.offset as usize;
let size = arch.size as usize;
let end = offset.saturating_add(size);
if end <= data.len() {
let macho = MachO::parse(data, offset)
.map_err(|e| {
EngineError::InvalidBinary {
let macho =
MachO::parse(data, offset).map_err(|e| EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
return parse_single_macho(
&macho, data, true,
);
return parse_single_macho(&macho, data, true);
}
}
Err(EngineError::InvalidBinary {
@ -97,8 +81,7 @@ fn parse_single_macho(
data: &[u8],
is_universal: bool,
) -> Result<FormatResult, EngineError> {
let architecture =
map_architecture(macho.header.cputype);
let architecture = map_architecture(macho.header.cputype);
let bits = if macho.is_64 { 64 } else { 32 };
let endianness = if macho.little_endian {
Endianness::Little
@ -107,25 +90,20 @@ fn parse_single_macho(
};
let entry_point = macho.entry;
let symbols: Vec<_> =
macho.symbols().flatten().collect();
let symbols: Vec<_> = macho.symbols().flatten().collect();
let is_stripped = symbols.is_empty();
let has_debug_info = macho.segments.iter().any(|seg| {
seg.name().is_ok_and(|n| n == "__DWARF")
});
let has_debug_info = macho
.segments
.iter()
.any(|seg| seg.name().is_ok_and(|n| n == "__DWARF"));
let is_pie = macho.header.flags & 0x0020_0000 != 0;
let sections = build_sections(macho, data);
let segments = build_segments(macho);
let anomalies = detect_common_anomalies(
&sections,
entry_point,
is_stripped,
);
let macho_info =
build_macho_info(macho, is_universal);
let anomalies = detect_common_anomalies(&sections, entry_point, is_stripped);
let macho_info = build_macho_info(macho, is_universal);
let function_hints: Vec<u64> = macho
.symbols()
@ -164,11 +142,7 @@ fn map_architecture(cputype: u32) -> Architecture {
CPU_TYPE_X86_64 => Architecture::X86_64,
CPU_TYPE_ARM => Architecture::Arm,
CPU_TYPE_ARM64 => Architecture::Aarch64,
other => {
Architecture::Other(format!(
"mach-cpu-{other:#x}"
))
}
other => Architecture::Other(format!("mach-cpu-{other:#x}")),
}
}
@ -184,14 +158,10 @@ fn file_type_name(filetype: u32) -> String {
}
}
fn cpu_subtype_name(
cputype: u32,
cpusubtype: u32,
) -> String {
fn cpu_subtype_name(cputype: u32, cpusubtype: u32) -> String {
let subtype = cpusubtype & 0x00FF_FFFF;
match cputype {
CPU_TYPE_X86 | CPU_TYPE_X86_64 => {
match subtype {
CPU_TYPE_X86 | CPU_TYPE_X86_64 => match subtype {
3 => "ALL".into(),
4 => "486".into(),
8 => "PENTIUM_3".into(),
@ -200,8 +170,7 @@ fn cpu_subtype_name(
11 => "ITANIUM".into(),
12 => "XEON".into(),
_ => format!("{subtype}"),
}
}
},
CPU_TYPE_ARM => match subtype {
6 => "v6".into(),
9 => "v7".into(),
@ -220,34 +189,23 @@ fn cpu_subtype_name(
}
}
fn build_sections(
macho: &MachO,
data: &[u8],
) -> Vec<SectionInfo> {
fn build_sections(macho: &MachO, data: &[u8]) -> Vec<SectionInfo> {
let mut sections = Vec::new();
for segment in macho.segments.iter() {
let initprot = segment.initprot;
let seg_permissions = SectionPermissions {
read: (initprot & VM_PROT_READ) != 0,
write: (initprot & VM_PROT_WRITE) != 0,
execute: (initprot & VM_PROT_EXECUTE)
!= 0,
execute: (initprot & VM_PROT_EXECUTE) != 0,
};
for section_result in segment.into_iter() {
let Ok((section, _section_data)) =
section_result
else {
let Ok((section, _section_data)) = section_result else {
continue;
};
let name = section
.name()
.unwrap_or("???")
.to_string();
let name = section.name().unwrap_or("???").to_string();
let raw_offset = section.offset as u64;
let raw_size = section.size;
let sha256 = compute_section_hash(
data, raw_offset, raw_size,
);
let sha256 = compute_section_hash(data, raw_offset, raw_size);
sections.push(SectionInfo {
name,
@ -268,16 +226,12 @@ fn build_segments(macho: &MachO) -> Vec<SegmentInfo> {
.segments
.iter()
.map(|seg| {
let name = seg
.name()
.ok()
.map(|n| n.to_string());
let name = seg.name().ok().map(|n| n.to_string());
let initprot = seg.initprot;
let permissions = SectionPermissions {
read: (initprot & VM_PROT_READ) != 0,
write: (initprot & VM_PROT_WRITE) != 0,
execute: (initprot & VM_PROT_EXECUTE)
!= 0,
execute: (initprot & VM_PROT_EXECUTE) != 0,
};
SegmentInfo {
@ -292,16 +246,9 @@ fn build_segments(macho: &MachO) -> Vec<SegmentInfo> {
.collect()
}
fn build_macho_info(
macho: &MachO,
is_universal: bool,
) -> MachOInfo {
let file_type =
file_type_name(macho.header.filetype);
let cpu_subtype = cpu_subtype_name(
macho.header.cputype,
macho.header.cpusubtype,
);
fn build_macho_info(macho: &MachO, is_universal: bool) -> MachOInfo {
let file_type = file_type_name(macho.header.filetype);
let cpu_subtype = cpu_subtype_name(macho.header.cputype, macho.header.cpusubtype);
let mut has_code_signature = false;
let mut has_function_starts = false;

View File

@ -35,9 +35,7 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::EngineError;
use crate::types::{
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
pub const SUSPICIOUS_SECTION_NAMES: &[(&str, &str)] = &[
("UPX0", "UPX packer"),
@ -192,37 +190,22 @@ pub struct MachOInfo {
pub has_function_starts: bool,
}
pub fn parse_format(
data: &[u8],
) -> Result<FormatResult, EngineError> {
let object =
goblin::Object::parse(data).map_err(|e| {
EngineError::InvalidBinary {
pub fn parse_format(data: &[u8]) -> Result<FormatResult, EngineError> {
let object = goblin::Object::parse(data).map_err(|e| EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
match &object {
goblin::Object::Elf(elf_obj) => {
elf::parse_elf(elf_obj, data)
}
goblin::Object::PE(pe_obj) => {
pe::parse_pe(pe_obj, data)
}
goblin::Object::Mach(mach_obj) => {
macho::parse_macho(mach_obj, data)
}
goblin::Object::Elf(elf_obj) => elf::parse_elf(elf_obj, data),
goblin::Object::PE(pe_obj) => pe::parse_pe(pe_obj, data),
goblin::Object::Mach(mach_obj) => macho::parse_macho(mach_obj, data),
_ => Err(EngineError::UnsupportedFormat {
format: "unknown".into(),
}),
}
}
fn compute_section_hash(
data: &[u8],
offset: u64,
size: u64,
) -> String {
fn compute_section_hash(data: &[u8], offset: u64, size: u64) -> String {
if size == 0 {
return String::new();
}
@ -251,53 +234,32 @@ fn detect_common_anomalies(
) -> Vec<FormatAnomaly> {
let mut anomalies = Vec::new();
let text_section =
sections.iter().find(|s| s.name == ".text");
let text_section = sections.iter().find(|s| s.name == ".text");
if let Some(text) = text_section {
let text_end =
text.virtual_address + text.virtual_size;
if entry_point != 0
&& (entry_point < text.virtual_address
|| entry_point >= text_end)
{
anomalies.push(
FormatAnomaly::EntryPointOutsideText {
let text_end = text.virtual_address + text.virtual_size;
if entry_point != 0 && (entry_point < text.virtual_address || entry_point >= text_end) {
anomalies.push(FormatAnomaly::EntryPointOutsideText {
ep: entry_point,
text_range: (
text.virtual_address,
text_end,
),
},
);
text_range: (text.virtual_address, text_end),
});
}
}
if let Some(last) = sections.last() {
let last_end =
last.virtual_address + last.virtual_size;
if entry_point >= last.virtual_address
&& entry_point < last_end
{
anomalies.push(
FormatAnomaly::EntryPointInLastSection {
let last_end = last.virtual_address + last.virtual_size;
if entry_point >= last.virtual_address && entry_point < last_end {
anomalies.push(FormatAnomaly::EntryPointInLastSection {
ep: entry_point,
section: last.name.clone(),
},
);
});
}
}
let ep_in_any = sections.iter().any(|s| {
entry_point >= s.virtual_address
&& entry_point
< s.virtual_address + s.virtual_size
entry_point >= s.virtual_address && entry_point < s.virtual_address + s.virtual_size
});
if !ep_in_any && entry_point != 0 {
anomalies.push(
FormatAnomaly::EntryPointOutsideSections {
ep: entry_point,
},
);
anomalies.push(FormatAnomaly::EntryPointOutsideSections { ep: entry_point });
}
for (idx, section) in sections.iter().enumerate() {
@ -308,47 +270,31 @@ fn detect_common_anomalies(
}
if section.name.is_empty() {
anomalies.push(
FormatAnomaly::EmptySectionName {
index: idx,
},
);
anomalies.push(FormatAnomaly::EmptySectionName { index: idx });
}
if let Some(reason) =
check_suspicious_name(&section.name)
{
anomalies.push(
FormatAnomaly::SuspiciousSectionName {
if let Some(reason) = check_suspicious_name(&section.name) {
anomalies.push(FormatAnomaly::SuspiciousSectionName {
name: section.name.clone(),
reason,
},
);
});
}
if section.permissions.execute
&& section.virtual_size == 0
{
anomalies.push(
FormatAnomaly::ZeroSizeCodeSection {
if section.permissions.execute && section.virtual_size == 0 {
anomalies.push(FormatAnomaly::ZeroSizeCodeSection {
name: section.name.clone(),
},
);
});
}
if section.raw_size > 0 {
let ratio = section.virtual_size as f64
/ section.raw_size as f64;
let ratio = section.virtual_size as f64 / section.raw_size as f64;
if ratio > VIRTUAL_RAW_RATIO_THRESHOLD {
anomalies.push(
FormatAnomaly::VirtualRawSizeMismatch {
anomalies.push(FormatAnomaly::VirtualRawSizeMismatch {
name: section.name.clone(),
virtual_size: section
.virtual_size,
virtual_size: section.virtual_size,
raw_size: section.raw_size,
ratio,
},
);
});
}
}
}

View File

@ -28,14 +28,11 @@
use goblin::pe::PE;
use super::{
FormatAnomaly, FormatResult, PeDllCharacteristics, PeInfo, SectionInfo, SegmentInfo,
compute_section_hash, detect_common_anomalies,
FormatAnomaly, FormatResult, PeDllCharacteristics, PeInfo,
SectionInfo, SegmentInfo,
};
use crate::error::EngineError;
use crate::types::{
Architecture, BinaryFormat, Endianness, SectionPermissions,
};
use crate::types::{Architecture, BinaryFormat, Endianness, SectionPermissions};
const COFF_MACHINE_I386: u16 = 0x14c;
const COFF_MACHINE_AMD64: u16 = 0x8664;
@ -48,8 +45,7 @@ const IMAGE_SCN_MEM_WRITE: u32 = 0x8000_0000;
const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
const IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE: u16 = 0x0040;
const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 =
0x0080;
const IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY: u16 = 0x0080;
const IMAGE_DLLCHARACTERISTICS_NX_COMPAT: u16 = 0x0100;
const IMAGE_DLLCHARACTERISTICS_NO_SEH: u16 = 0x0400;
const IMAGE_DLLCHARACTERISTICS_GUARD_CF: u16 = 0x4000;
@ -70,43 +66,30 @@ const PE_TIMESTAMP_MAX_VALID: u32 = 4_102_444_800;
const RICH_SIGNATURE: &[u8] = b"Rich";
pub fn parse_pe(
pe: &PE,
data: &[u8],
) -> Result<FormatResult, EngineError> {
let architecture = map_architecture(
pe.header.coff_header.machine,
);
pub fn parse_pe(pe: &PE, data: &[u8]) -> Result<FormatResult, EngineError> {
let architecture = map_architecture(pe.header.coff_header.machine);
let bits = if pe.is_64 { 64 } else { 32 };
let endianness = Endianness::Little;
let entry_point = pe.entry as u64;
let is_stripped = pe.debug_data.is_none();
let optional = pe.header.optional_header.as_ref();
let dll_chars = optional.map_or(0, |oh| {
oh.windows_fields.dll_characteristics
});
let is_pie = (dll_chars
& IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
!= 0;
let dll_chars = optional.map_or(0, |oh| oh.windows_fields.dll_characteristics);
let is_pie = (dll_chars & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) != 0;
let has_debug_info = pe.debug_data.is_some();
let sections = build_sections(pe, data);
let segments = build_segments(pe);
let timestamp =
pe.header.coff_header.time_date_stamp;
let image_base = optional
.map_or(0, |oh| oh.windows_fields.image_base);
let subsystem_raw = optional
.map_or(0, |oh| oh.windows_fields.subsystem);
let timestamp = pe.header.coff_header.time_date_stamp;
let image_base = optional.map_or(0, |oh| oh.windows_fields.image_base);
let subsystem_raw = optional.map_or(0, |oh| oh.windows_fields.subsystem);
let linker_version = optional.map_or_else(
|| "0.0".into(),
|oh| {
format!(
"{}.{}",
oh.standard_fields.major_linker_version,
oh.standard_fields.minor_linker_version,
oh.standard_fields.major_linker_version, oh.standard_fields.minor_linker_version,
)
},
);
@ -117,47 +100,29 @@ pub fn parse_pe(
let max_section_end = pe
.sections
.iter()
.map(|s| {
s.pointer_to_raw_data as u64
+ s.size_of_raw_data as u64
})
.map(|s| s.pointer_to_raw_data as u64 + s.size_of_raw_data as u64)
.max()
.unwrap_or(0);
let file_size = data.len() as u64;
let has_overlay =
max_section_end > 0 && max_section_end < file_size;
let has_overlay = max_section_end > 0 && max_section_end < file_size;
let overlay_size = if has_overlay {
file_size - max_section_end
} else {
0
};
let pe_offset =
pe.header.dos_header.pe_pointer as usize;
let rich_header_present = detect_rich_header(
data,
pe_offset,
);
let pe_offset = pe.header.dos_header.pe_pointer as usize;
let rich_header_present = detect_rich_header(data, pe_offset);
let pe_info = PeInfo {
image_base,
subsystem: subsystem_name(subsystem_raw),
dll_characteristics: PeDllCharacteristics {
aslr: (dll_chars
& IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
!= 0,
dep: (dll_chars
& IMAGE_DLLCHARACTERISTICS_NX_COMPAT)
!= 0,
cfg: (dll_chars
& IMAGE_DLLCHARACTERISTICS_GUARD_CF)
!= 0,
no_seh: (dll_chars
& IMAGE_DLLCHARACTERISTICS_NO_SEH)
!= 0,
force_integrity: (dll_chars
& IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY)
!= 0,
aslr: (dll_chars & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) != 0,
dep: (dll_chars & IMAGE_DLLCHARACTERISTICS_NX_COMPAT) != 0,
cfg: (dll_chars & IMAGE_DLLCHARACTERISTICS_GUARD_CF) != 0,
no_seh: (dll_chars & IMAGE_DLLCHARACTERISTICS_NO_SEH) != 0,
force_integrity: (dll_chars & IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY) != 0,
},
timestamp,
linker_version,
@ -167,11 +132,7 @@ pub fn parse_pe(
rich_header_present,
};
let mut anomalies = detect_common_anomalies(
&sections,
entry_point,
is_stripped,
);
let mut anomalies = detect_common_anomalies(&sections, entry_point, is_stripped);
detect_pe_anomalies(
&mut anomalies,
pe,
@ -213,15 +174,9 @@ fn map_architecture(machine: u16) -> Architecture {
match machine {
COFF_MACHINE_I386 => Architecture::X86,
COFF_MACHINE_AMD64 => Architecture::X86_64,
COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => {
Architecture::Arm
}
COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => Architecture::Arm,
COFF_MACHINE_ARM64 => Architecture::Aarch64,
other => {
Architecture::Other(format!(
"pe-machine-{other:#x}"
))
}
other => Architecture::Other(format!("pe-machine-{other:#x}")),
}
}
@ -232,53 +187,33 @@ fn subsystem_name(subsystem: u16) -> String {
IMAGE_SUBSYSTEM_WINDOWS_GUI => "GUI".into(),
IMAGE_SUBSYSTEM_WINDOWS_CUI => "Console".into(),
IMAGE_SUBSYSTEM_POSIX_CUI => "POSIX".into(),
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI => {
"Windows CE".into()
}
IMAGE_SUBSYSTEM_EFI_APPLICATION => {
"EFI Application".into()
}
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER => {
"EFI Boot Service Driver".into()
}
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER => {
"EFI Runtime Driver".into()
}
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI => "Windows CE".into(),
IMAGE_SUBSYSTEM_EFI_APPLICATION => "EFI Application".into(),
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER => "EFI Boot Service Driver".into(),
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER => "EFI Runtime Driver".into(),
IMAGE_SUBSYSTEM_XBOX => "Xbox".into(),
other => format!("Unknown({other})"),
}
}
fn build_sections(
pe: &PE,
data: &[u8],
) -> Vec<SectionInfo> {
fn build_sections(pe: &PE, data: &[u8]) -> Vec<SectionInfo> {
pe.sections
.iter()
.map(|section| {
let name = section
.name()
.unwrap_or_default()
.to_string();
let raw_offset =
section.pointer_to_raw_data as u64;
let raw_size =
section.size_of_raw_data as u64;
let name = section.name().unwrap_or_default().to_string();
let raw_offset = section.pointer_to_raw_data as u64;
let raw_size = section.size_of_raw_data as u64;
let chars = section.characteristics;
let permissions = SectionPermissions {
read: (chars & IMAGE_SCN_MEM_READ) != 0,
write: (chars & IMAGE_SCN_MEM_WRITE) != 0,
execute: (chars & IMAGE_SCN_MEM_EXECUTE)
!= 0,
execute: (chars & IMAGE_SCN_MEM_EXECUTE) != 0,
};
let sha256 = compute_section_hash(
data, raw_offset, raw_size,
);
let sha256 = compute_section_hash(data, raw_offset, raw_size);
SectionInfo {
name,
virtual_address: section.virtual_address
as u64,
virtual_address: section.virtual_address as u64,
virtual_size: section.virtual_size as u64,
raw_offset,
raw_size,
@ -293,10 +228,7 @@ fn build_segments(_pe: &PE) -> Vec<SegmentInfo> {
Vec::new()
}
fn detect_rich_header(
data: &[u8],
pe_offset: usize,
) -> bool {
fn detect_rich_header(data: &[u8], pe_offset: usize) -> bool {
let end = pe_offset.min(data.len());
data[..end]
.windows(RICH_SIGNATURE.len())
@ -313,34 +245,24 @@ fn detect_pe_anomalies(
file_size: u64,
) {
if timestamp == 0 {
anomalies.push(
FormatAnomaly::SuspiciousTimestamp {
anomalies.push(FormatAnomaly::SuspiciousTimestamp {
value: timestamp,
reason: "zeroed timestamp".into(),
},
);
});
} else if timestamp < PE_TIMESTAMP_MIN_VALID {
anomalies.push(
FormatAnomaly::SuspiciousTimestamp {
anomalies.push(FormatAnomaly::SuspiciousTimestamp {
value: timestamp,
reason: "timestamp before 1990".into(),
},
);
});
} else if timestamp > PE_TIMESTAMP_MAX_VALID {
anomalies.push(
FormatAnomaly::SuspiciousTimestamp {
anomalies.push(FormatAnomaly::SuspiciousTimestamp {
value: timestamp,
reason: "timestamp after 2100".into(),
},
);
});
}
if has_tls {
anomalies.push(
FormatAnomaly::TlsCallbacksPresent {
count: 1,
},
);
anomalies.push(FormatAnomaly::TlsCallbacksPresent { count: 1 });
}
if pe.imports.is_empty() {

View File

@ -52,8 +52,7 @@ pub struct AnalysisEngine {
impl AnalysisEngine {
pub fn new() -> Result<Self, EngineError> {
let passes: Vec<Box<dyn pass::AnalysisPass>> =
vec![
let passes: Vec<Box<dyn pass::AnalysisPass>> = vec![
Box::new(FormatPass),
Box::new(ImportPass),
Box::new(StringPass),
@ -67,23 +66,16 @@ impl AnalysisEngine {
Ok(Self { pass_manager })
}
pub fn analyze(
&self,
data: &[u8],
file_name: &str,
) -> (AnalysisContext, PassReport) {
pub fn analyze(&self, data: &[u8], file_name: &str) -> (AnalysisContext, PassReport) {
let sha256 = compute_sha256(data);
let file_size = data.len() as u64;
let mut ctx = AnalysisContext::new(
BinarySource::Buffered(Arc::from(
data.to_vec(),
)),
BinarySource::Buffered(Arc::from(data.to_vec())),
sha256,
file_name.to_string(),
file_size,
);
let report =
self.pass_manager.run_all(&mut ctx);
let report = self.pass_manager.run_all(&mut ctx);
(ctx, report)
}
}

View File

@ -33,10 +33,7 @@ mod private {
pub trait AnalysisPass: private::Sealed + Send + Sync {
fn name(&self) -> &'static str;
fn dependencies(&self) -> &[&'static str];
fn run(
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError>;
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError>;
}
#[derive(Debug, Clone)]
@ -58,10 +55,7 @@ impl PassReport {
}
pub fn failed_passes(&self) -> Vec<&PassOutcome> {
self.outcomes
.iter()
.filter(|o| !o.success)
.collect()
self.outcomes.iter().filter(|o| !o.success).collect()
}
}
@ -71,33 +65,23 @@ pub struct PassManager {
}
impl PassManager {
pub fn new(
passes: Vec<Box<dyn AnalysisPass>>,
) -> Self {
pub fn new(passes: Vec<Box<dyn AnalysisPass>>) -> Self {
let order = topological_order(&passes);
Self { passes, order }
}
pub fn run_all(
&self,
ctx: &mut AnalysisContext,
) -> PassReport {
pub fn run_all(&self, ctx: &mut AnalysisContext) -> PassReport {
let mut outcomes = Vec::with_capacity(self.passes.len());
for &idx in &self.order {
let pass = &self.passes[idx];
let start = Instant::now();
let result = pass.run(ctx);
let duration_ms =
start.elapsed().as_millis() as u64;
let duration_ms = start.elapsed().as_millis() as u64;
let outcome = match result {
Ok(()) => {
tracing::info!(
pass = pass.name(),
duration_ms,
"pass completed"
);
tracing::info!(pass = pass.name(), duration_ms, "pass completed");
PassOutcome {
name: pass.name(),
success: true,
@ -128,9 +112,7 @@ impl PassManager {
}
}
fn topological_order(
passes: &[Box<dyn AnalysisPass>],
) -> Vec<usize> {
fn topological_order(passes: &[Box<dyn AnalysisPass>]) -> Vec<usize> {
let name_to_idx: HashMap<&str, usize> = passes
.iter()
.enumerate()
@ -143,8 +125,7 @@ fn topological_order(
for (idx, pass) in passes.iter().enumerate() {
for dep_name in pass.dependencies() {
if let Some(&dep_idx) = name_to_idx.get(dep_name)
{
if let Some(&dep_idx) = name_to_idx.get(dep_name) {
adjacency[dep_idx].push(idx);
in_degree[idx] += 1;
}
@ -204,10 +185,7 @@ mod tests {
&self.deps
}
fn run(
&self,
_ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
fn run(&self, _ctx: &mut AnalysisContext) -> Result<(), EngineError> {
self.log.lock().unwrap().push(self.name);
if self.should_fail {
return Err(EngineError::PassFailed {
@ -221,9 +199,7 @@ mod tests {
fn make_ctx() -> AnalysisContext {
AnalysisContext::new(
crate::context::BinarySource::Buffered(
Arc::from(vec![0u8; 4]),
),
crate::context::BinarySource::Buffered(Arc::from(vec![0u8; 4])),
"deadbeef".into(),
"test.bin".into(),
4,
@ -295,16 +271,10 @@ mod tests {
let report = manager.run_all(&mut ctx);
let execution_order = log.lock().unwrap().clone();
assert_eq!(
execution_order,
vec!["first", "second", "third"]
);
assert_eq!(execution_order, vec!["first", "second", "third"]);
assert!(!report.all_succeeded());
assert_eq!(report.failed_passes().len(), 1);
assert_eq!(
report.failed_passes()[0].name,
"second"
);
assert_eq!(report.failed_passes()[0].name, "second");
}
#[test]
@ -343,14 +313,10 @@ mod tests {
manager.run_all(&mut ctx);
let order = log.lock().unwrap().clone();
let format_pos =
order.iter().position(|&n| n == "format").unwrap();
let imports_pos =
order.iter().position(|&n| n == "imports").unwrap();
let entropy_pos =
order.iter().position(|&n| n == "entropy").unwrap();
let score_pos =
order.iter().position(|&n| n == "score").unwrap();
let format_pos = order.iter().position(|&n| n == "format").unwrap();
let imports_pos = order.iter().position(|&n| n == "imports").unwrap();
let entropy_pos = order.iter().position(|&n| n == "entropy").unwrap();
let score_pos = order.iter().position(|&n| n == "score").unwrap();
assert!(format_pos < imports_pos);
assert!(format_pos < entropy_pos);
@ -385,14 +351,12 @@ mod tests {
fn reports_duration() {
let log = Arc::new(Mutex::new(Vec::new()));
let passes: Vec<Box<dyn AnalysisPass>> = vec![
Box::new(MockPass {
let passes: Vec<Box<dyn AnalysisPass>> = vec![Box::new(MockPass {
name: "fast",
deps: vec![],
log: Arc::clone(&log),
should_fail: false,
}),
];
})];
let manager = PassManager::new(passes);
let mut ctx = make_ctx();

View File

@ -41,23 +41,16 @@
// FlowControlType
// error.rs - EngineError
use std::collections::{
BTreeMap, HashMap, HashSet, VecDeque,
};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use iced_x86::{
Decoder, DecoderOptions, FlowControl, Formatter,
Instruction, IntelFormatter,
};
use iced_x86::{Decoder, DecoderOptions, FlowControl, Formatter, Instruction, IntelFormatter};
use serde::{Deserialize, Serialize};
use crate::context::AnalysisContext;
use crate::error::EngineError;
use crate::formats::SectionInfo;
use crate::pass::{AnalysisPass, Sealed};
use crate::types::{
Architecture, CfgEdgeType, FlowControlType,
};
use crate::types::{Architecture, CfgEdgeType, FlowControlType};
const MAX_FUNCTIONS: usize = 1000;
const MAX_INSTRUCTIONS: usize = 50_000;
@ -103,9 +96,7 @@ pub struct InstructionInfo {
pub flow_control: FlowControlType,
}
#[derive(
Debug, Clone, Default, Serialize, Deserialize,
)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FunctionCfg {
pub nodes: Vec<CfgNode>,
pub edges: Vec<CfgEdge>,
@ -139,12 +130,9 @@ impl AnalysisPass for DisasmPass {
&["format"]
}
fn run(
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
let format_result = ctx
.format_result
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
let format_result =
ctx.format_result
.as_ref()
.ok_or_else(|| EngineError::MissingDependency {
pass: "disasm".into(),
@ -157,10 +145,7 @@ impl AnalysisPass for DisasmPass {
Architecture::X86_64 => 64,
_ => {
ctx.disassembly_result =
Some(empty_result(
format_result.bits,
format_result.entry_point,
));
Some(empty_result(format_result.bits, format_result.entry_point));
return Ok(());
}
};
@ -170,26 +155,15 @@ impl AnalysisPass for DisasmPass {
let entry_point = format_result.entry_point;
let mut seeds = vec![entry_point];
seeds.extend_from_slice(
&format_result.function_hints,
);
seeds.extend_from_slice(&format_result.function_hints);
let result = disassemble(
data,
sections,
bits,
entry_point,
&seeds,
);
let result = disassemble(data, sections, bits, entry_point, &seeds);
ctx.disassembly_result = Some(result);
Ok(())
}
}
fn empty_result(
bits: u8,
entry_point: u64,
) -> DisassemblyResult {
fn empty_result(bits: u8, entry_point: u64) -> DisassemblyResult {
DisassemblyResult {
functions: Vec::new(),
total_instructions: 0,
@ -214,26 +188,20 @@ fn disassemble(
let mut functions = Vec::new();
let mut visited_functions = HashSet::new();
let mut total_instructions = 0;
let mut function_queue: VecDeque<u64> =
seeds.iter().copied().collect();
let mut function_queue: VecDeque<u64> = seeds.iter().copied().collect();
while let Some(func_addr) = function_queue.pop_front()
{
if functions.len() >= MAX_FUNCTIONS
|| total_instructions >= MAX_INSTRUCTIONS
{
while let Some(func_addr) = function_queue.pop_front() {
if functions.len() >= MAX_FUNCTIONS || total_instructions >= MAX_INSTRUCTIONS {
break;
}
if !visited_functions.insert(func_addr) {
continue;
}
if vaddr_to_offset(sections, func_addr).is_none()
{
if vaddr_to_offset(sections, func_addr).is_none() {
continue;
}
let (func_info, discovered_calls) =
disassemble_function(
let (func_info, discovered_calls) = disassemble_function(
data,
sections,
&exec_sections,
@ -273,8 +241,7 @@ fn disassemble_function(
is_entry_point: bool,
instruction_budget: usize,
) -> (FunctionInfo, Vec<u64>) {
let mut decoded: BTreeMap<u64, DecodedInstruction> =
BTreeMap::new();
let mut decoded: BTreeMap<u64, DecodedInstruction> = BTreeMap::new();
let mut block_leaders: HashSet<u64> = HashSet::new();
let mut worklist: VecDeque<u64> = VecDeque::new();
let mut visited: HashSet<u64> = HashSet::new();
@ -292,10 +259,7 @@ fn disassemble_function(
break;
}
let offset = match vaddr_to_offset(
all_sections,
addr,
) {
let offset = match vaddr_to_offset(all_sections, addr) {
Some(o) => o as usize,
None => continue,
};
@ -310,17 +274,10 @@ fn disassemble_function(
}
let slice = &data[offset..];
let mut decoder = Decoder::with_ip(
bits,
slice,
addr,
DecoderOptions::NONE,
);
let mut decoder = Decoder::with_ip(bits, slice, addr, DecoderOptions::NONE);
let mut instr = Instruction::default();
while decoder.can_decode()
&& decoded.len() < instruction_budget
{
while decoder.can_decode() && decoded.len() < instruction_budget {
decoder.decode_out(&mut instr);
let ip = instr.ip();
@ -328,30 +285,21 @@ fn disassemble_function(
break;
}
if ip != addr
&& block_leaders.contains(&ip)
{
if ip != addr && block_leaders.contains(&ip) {
break;
}
let fc = instr.flow_control();
let mnemonic = format!("{:?}", instr.mnemonic())
.to_ascii_lowercase();
let mnemonic = format!("{:?}", instr.mnemonic()).to_ascii_lowercase();
let mut operands_str = String::new();
formatter
.format(&instr, &mut operands_str);
formatter.format(&instr, &mut operands_str);
let operands = operands_str
.split_once(' ')
.map_or(String::new(), |(_, ops)| {
ops.to_string()
});
.map_or(String::new(), |(_, ops)| ops.to_string());
let instr_bytes = &data
[offset + (ip - addr) as usize
..offset
+ (ip - addr) as usize
+ instr.len()];
let instr_bytes =
&data[offset + (ip - addr) as usize..offset + (ip - addr) as usize + instr.len()];
let flow_type = map_flow_control(fc);
@ -374,13 +322,10 @@ fn disassemble_function(
match fc {
FlowControl::ConditionalBranch => {
let target =
instr.near_branch_target();
let target = instr.near_branch_target();
let fall = instr.next_ip();
if let Some(di) =
decoded.get_mut(&ip)
{
if let Some(di) = decoded.get_mut(&ip) {
di.branch_target = Some(target);
di.fallthrough = Some(fall);
}
@ -392,11 +337,8 @@ fn disassemble_function(
break;
}
FlowControl::UnconditionalBranch => {
let target =
instr.near_branch_target();
if let Some(di) =
decoded.get_mut(&ip)
{
let target = instr.near_branch_target();
if let Some(di) = decoded.get_mut(&ip) {
di.branch_target = Some(target);
}
block_leaders.insert(target);
@ -410,36 +352,24 @@ fn disassemble_function(
break;
}
FlowControl::Call => {
let target =
instr.near_branch_target();
let target = instr.near_branch_target();
if target != 0 {
discovered_calls.push(target);
}
}
FlowControl::IndirectCall => {}
FlowControl::Next
| FlowControl::XbeginXabortXend => {}
FlowControl::Next | FlowControl::XbeginXabortXend => {}
}
}
}
let basic_blocks = build_basic_blocks(
&decoded,
&block_leaders,
);
let instruction_count: usize = basic_blocks
.iter()
.map(|bb| bb.instruction_count)
.sum();
let basic_blocks = build_basic_blocks(&decoded, &block_leaders);
let instruction_count: usize = basic_blocks.iter().map(|bb| bb.instruction_count).sum();
let size = if let (Some(first), Some(last)) = (
decoded.keys().next(),
decoded.keys().next_back(),
) {
let size =
if let (Some(first), Some(last)) = (decoded.keys().next(), decoded.keys().next_back()) {
if let Some(last_instr) = decoded.get(last) {
last_instr.info.address
+ last_instr.info.size as u64
- first
last_instr.info.address + last_instr.info.size as u64 - first
} else {
0
}
@ -447,8 +377,7 @@ fn disassemble_function(
0
};
let cfg = if instruction_count <= CFG_INSTRUCTION_LIMIT
{
let cfg = if instruction_count <= CFG_INSTRUCTION_LIMIT {
build_cfg(&basic_blocks)
} else {
FunctionCfg::default()
@ -483,14 +412,11 @@ fn build_basic_blocks(
}
let mut blocks: Vec<BasicBlockInfo> = Vec::new();
let mut current_instrs: Vec<InstructionInfo> =
Vec::new();
let mut current_instrs: Vec<InstructionInfo> = Vec::new();
let mut block_start: Option<u64> = None;
for (&addr, di) in decoded {
if leaders.contains(&addr)
&& !current_instrs.is_empty()
{
if leaders.contains(&addr) && !current_instrs.is_empty() {
let bb = finalize_block(
&current_instrs,
block_start.unwrap_or(addr),
@ -527,35 +453,24 @@ fn build_basic_blocks(
}
}
if !current_instrs.is_empty() {
if let Some(start) = block_start {
let bb = finalize_block(
&current_instrs,
start,
decoded,
leaders,
);
if !current_instrs.is_empty()
&& let Some(start) = block_start
{
let bb = finalize_block(&current_instrs, start, decoded, leaders);
blocks.push(bb);
}
}
let block_starts: HashSet<u64> =
blocks.iter().map(|b| b.start_address).collect();
let block_starts: HashSet<u64> = blocks.iter().map(|b| b.start_address).collect();
for block in &mut blocks {
block
.successors
.retain(|s| block_starts.contains(s));
block.successors.retain(|s| block_starts.contains(s));
}
let predecessor_map: HashMap<u64, Vec<u64>> = {
let mut map: HashMap<u64, Vec<u64>> =
HashMap::new();
let mut map: HashMap<u64, Vec<u64>> = HashMap::new();
for block in &blocks {
for &succ in &block.successors {
map.entry(succ)
.or_default()
.push(block.start_address);
map.entry(succ).or_default().push(block.start_address);
}
}
map
@ -578,8 +493,7 @@ fn finalize_block(
leaders: &HashSet<u64>,
) -> BasicBlockInfo {
let last = instructions.last().unwrap();
let end_address =
last.address + last.size as u64 - 1;
let end_address = last.address + last.size as u64 - 1;
let mut successors = Vec::new();
let last_addr = last.address;
@ -591,14 +505,10 @@ fn finalize_block(
successors.push(fall);
} else if !matches!(
di.info.flow_control,
FlowControlType::Branch
| FlowControlType::Return
| FlowControlType::Interrupt
FlowControlType::Branch | FlowControlType::Return | FlowControlType::Interrupt
) {
let next = di.next_ip;
if leaders.contains(&next)
|| decoded.contains_key(&next)
{
if leaders.contains(&next) || decoded.contains_key(&next) {
successors.push(next);
}
}
@ -614,9 +524,7 @@ fn finalize_block(
}
}
fn build_cfg(
blocks: &[BasicBlockInfo],
) -> FunctionCfg {
fn build_cfg(blocks: &[BasicBlockInfo]) -> FunctionCfg {
let mut nodes = Vec::new();
let mut edges = Vec::new();
@ -635,35 +543,23 @@ fn build_cfg(
nodes.push(CfgNode {
id: block.start_address,
label: format!(
"0x{:x}",
block.start_address
),
label: format!("0x{:x}", block.start_address),
instruction_count: block.instruction_count,
instructions_preview: preview,
});
let last_instr = block.instructions.last();
for &succ in &block.successors {
let edge_type =
if let Some(last) = last_instr {
let edge_type = if let Some(last) = last_instr {
match last.flow_control {
FlowControlType::ConditionalBranch => {
if succ
== block
.successors
.first()
.copied()
.unwrap_or(0)
{
if succ == block.successors.first().copied().unwrap_or(0) {
CfgEdgeType::ConditionalTrue
} else {
CfgEdgeType::ConditionalFalse
}
}
FlowControlType::Branch => {
CfgEdgeType::Unconditional
}
FlowControlType::Branch => CfgEdgeType::Unconditional,
_ => CfgEdgeType::Fallthrough,
}
} else {
@ -681,99 +577,54 @@ fn build_cfg(
FunctionCfg { nodes, edges }
}
fn vaddr_to_offset(
sections: &[SectionInfo],
vaddr: u64,
) -> Option<u64> {
fn vaddr_to_offset(sections: &[SectionInfo], vaddr: u64) -> Option<u64> {
sections.iter().find_map(|s| {
if s.raw_size > 0
&& vaddr >= s.virtual_address
&& vaddr
< s.virtual_address + s.virtual_size
&& vaddr < s.virtual_address + s.virtual_size
{
Some(
s.raw_offset
+ (vaddr - s.virtual_address),
)
Some(s.raw_offset + (vaddr - s.virtual_address))
} else {
None
}
})
}
fn is_in_exec_section(
exec_sections: &[&SectionInfo],
vaddr: u64,
) -> bool {
exec_sections.iter().any(|s| {
vaddr >= s.virtual_address
&& vaddr
< s.virtual_address + s.virtual_size
})
fn is_in_exec_section(exec_sections: &[&SectionInfo], vaddr: u64) -> bool {
exec_sections
.iter()
.any(|s| vaddr >= s.virtual_address && vaddr < s.virtual_address + s.virtual_size)
}
fn map_flow_control(
fc: FlowControl,
) -> FlowControlType {
fn map_flow_control(fc: FlowControl) -> FlowControlType {
match fc {
FlowControl::Next
| FlowControl::XbeginXabortXend => {
FlowControlType::Next
}
FlowControl::UnconditionalBranch
| FlowControl::IndirectBranch => {
FlowControlType::Branch
}
FlowControl::ConditionalBranch => {
FlowControlType::ConditionalBranch
}
FlowControl::Call
| FlowControl::IndirectCall => {
FlowControlType::Call
}
FlowControl::Next | FlowControl::XbeginXabortXend => FlowControlType::Next,
FlowControl::UnconditionalBranch | FlowControl::IndirectBranch => FlowControlType::Branch,
FlowControl::ConditionalBranch => FlowControlType::ConditionalBranch,
FlowControl::Call | FlowControl::IndirectCall => FlowControlType::Call,
FlowControl::Return => FlowControlType::Return,
FlowControl::Interrupt
| FlowControl::Exception => {
FlowControlType::Interrupt
}
FlowControl::Interrupt | FlowControl::Exception => FlowControlType::Interrupt,
}
}
pub fn disassemble_code(
code: &[u8],
base_addr: u64,
bits: u32,
) -> Vec<InstructionInfo> {
let mut decoder = Decoder::with_ip(
bits,
code,
base_addr,
DecoderOptions::NONE,
);
pub fn disassemble_code(code: &[u8], base_addr: u64, bits: u32) -> Vec<InstructionInfo> {
let mut decoder = Decoder::with_ip(bits, code, base_addr, DecoderOptions::NONE);
let mut formatter = IntelFormatter::new();
let mut instr = Instruction::default();
let mut result = Vec::new();
while decoder.can_decode() {
decoder.decode_out(&mut instr);
let mnemonic = format!(
"{:?}",
instr.mnemonic()
)
.to_ascii_lowercase();
let mnemonic = format!("{:?}", instr.mnemonic()).to_ascii_lowercase();
let mut full = String::new();
formatter.format(&instr, &mut full);
let operands = full
.split_once(' ')
.map_or(String::new(), |(_, ops)| {
ops.to_string()
});
.map_or(String::new(), |(_, ops)| ops.to_string());
let start =
(instr.ip() - base_addr) as usize;
let bytes =
code[start..start + instr.len()].to_vec();
let start = (instr.ip() - base_addr) as usize;
let bytes = code[start..start + instr.len()].to_vec();
result.push(InstructionInfo {
address: instr.ip(),
@ -781,9 +632,7 @@ pub fn disassemble_code(
mnemonic,
operands,
size: instr.len() as u8,
flow_control: map_flow_control(
instr.flow_control(),
),
flow_control: map_flow_control(instr.flow_control()),
});
}
@ -799,13 +648,8 @@ mod tests {
use crate::types::SectionPermissions;
fn load_fixture(name: &str) -> Vec<u8> {
let path = format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
}
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -820,27 +664,17 @@ mod tests {
#[test]
fn disassemble_simple_function() {
let code: &[u8] = &[
0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0,
0x5D, 0xC3,
];
let instrs =
disassemble_code(code, 0x1000, 64);
let code: &[u8] = &[0x55, 0x48, 0x89, 0xE5, 0x31, 0xC0, 0x5D, 0xC3];
let instrs = disassemble_code(code, 0x1000, 64);
assert_eq!(instrs.len(), 5);
assert_eq!(instrs[0].mnemonic, "push");
assert_eq!(instrs[4].mnemonic, "ret");
assert_eq!(
instrs[4].flow_control,
FlowControlType::Return
);
assert_eq!(instrs[4].flow_control, FlowControlType::Return);
}
#[test]
fn basic_block_split_on_branch() {
let code: &[u8] = &[
0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02,
0x31, 0xC9, 0xC3,
];
let code: &[u8] = &[0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, 0x31, 0xC9, 0xC3];
let sections = vec![SectionInfo {
name: ".text".into(),
@ -856,13 +690,7 @@ mod tests {
sha256: String::new(),
}];
let result = disassemble(
code,
&sections,
64,
0x1000,
&[0x1000],
);
let result = disassemble(code, &sections, 64, 0x1000, &[0x1000]);
assert!(!result.functions.is_empty());
let func = &result.functions[0];
assert!(
@ -874,10 +702,7 @@ mod tests {
#[test]
fn cfg_edges_conditional() {
let code: &[u8] = &[
0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02,
0x31, 0xC9, 0xC3,
];
let code: &[u8] = &[0x31, 0xC0, 0x85, 0xC0, 0x74, 0x02, 0x31, 0xC9, 0xC3];
let sections = vec![SectionInfo {
name: ".text".into(),
@ -893,35 +718,21 @@ mod tests {
sha256: String::new(),
}];
let result = disassemble(
code,
&sections,
64,
0x1000,
&[0x1000],
);
let result = disassemble(code, &sections, 64, 0x1000, &[0x1000]);
let func = &result.functions[0];
assert!(
!func.cfg.edges.is_empty(),
"CFG should have edges"
);
assert!(
!func.cfg.nodes.is_empty(),
"CFG should have nodes"
);
assert!(!func.cfg.edges.is_empty(), "CFG should have edges");
assert!(!func.cfg.nodes.is_empty(), "CFG should have nodes");
}
#[test]
fn non_x86_returns_empty() {
let data = vec![0u8; 64];
let mut ctx = make_ctx(data);
ctx.format_result =
Some(crate::formats::FormatResult {
ctx.format_result = Some(crate::formats::FormatResult {
format: crate::types::BinaryFormat::Elf,
architecture: Architecture::Aarch64,
bits: 64,
endianness:
crate::types::Endianness::Little,
endianness: crate::types::Endianness::Little,
entry_point: 0x1000,
is_stripped: false,
is_pie: false,
@ -946,28 +757,22 @@ mod tests {
let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data);
crate::passes::format::FormatPass
.run(&mut ctx)
.unwrap();
crate::passes::format::FormatPass.run(&mut ctx).unwrap();
DisasmPass.run(&mut ctx).unwrap();
let result =
ctx.disassembly_result.as_ref().unwrap();
let result = ctx.disassembly_result.as_ref().unwrap();
assert!(
result.total_functions > 0,
"should find at least one function"
);
assert!(result.total_instructions > 0);
let entry_func = result.functions.iter().find(
|f| {
f.address
== result.entry_function_address
},
);
let entry_func = result
.functions
.iter()
.find(|f| f.address == result.entry_function_address);
assert!(
entry_func.is_some()
|| !result.functions.is_empty(),
entry_func.is_some() || !result.functions.is_empty(),
"should have disassembled functions"
);
}
@ -977,9 +782,7 @@ mod tests {
let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data);
crate::passes::format::FormatPass
.run(&mut ctx)
.unwrap();
crate::passes::format::FormatPass.run(&mut ctx).unwrap();
assert!(ctx.disassembly_result.is_none());
DisasmPass.run(&mut ctx).unwrap();

View File

@ -42,9 +42,7 @@ use crate::context::AnalysisContext;
use crate::error::EngineError;
use crate::formats::SectionInfo;
use crate::pass::{AnalysisPass, Sealed};
use crate::types::{
EntropyClassification, EntropyFlag,
};
use crate::types::{EntropyClassification, EntropyFlag};
const PLAINTEXT_MAX: f64 = 3.5;
const NATIVE_CODE_MAX: f64 = 6.0;
@ -120,12 +118,9 @@ impl AnalysisPass for EntropyPass {
&["format"]
}
fn run(
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
let format_result = ctx
.format_result
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
let format_result =
ctx.format_result
.as_ref()
.ok_or_else(|| EngineError::MissingDependency {
pass: "entropy".into(),
@ -133,21 +128,13 @@ impl AnalysisPass for EntropyPass {
})?;
let data = ctx.data();
let result = analyze_entropy(
data,
&format_result.sections,
format_result.entry_point,
);
let result = analyze_entropy(data, &format_result.sections, format_result.entry_point);
ctx.entropy_result = Some(result);
Ok(())
}
}
fn analyze_entropy(
data: &[u8],
sections: &[SectionInfo],
entry_point: u64,
) -> EntropyResult {
fn analyze_entropy(data: &[u8], sections: &[SectionInfo], entry_point: u64) -> EntropyResult {
let overall_entropy = shannon_entropy(data);
let mut section_entropies = Vec::new();
let mut packing_indicators = Vec::new();
@ -155,11 +142,7 @@ fn analyze_entropy(
let mut structural_count = 0;
for section in sections {
let section_data = read_section_data(
data,
section.raw_offset,
section.raw_size,
);
let section_data = read_section_data(data, section.raw_offset, section.raw_size);
let entropy = if section_data.is_empty() {
0.0
} else {
@ -167,8 +150,7 @@ fn analyze_entropy(
};
let classification = classify_entropy(entropy);
let vr_ratio = if section.raw_size > 0 {
section.virtual_size as f64
/ section.raw_size as f64
section.virtual_size as f64 / section.raw_size as f64
} else {
0.0
};
@ -181,24 +163,18 @@ fn analyze_entropy(
if vr_ratio > VIRTUAL_RAW_RATIO_THRESHOLD {
flags.push(EntropyFlag::HighVirtualToRawRatio);
}
if section.raw_size == 0
&& section.virtual_size > 0
{
if section.raw_size == 0 && section.virtual_size > 0 {
flags.push(EntropyFlag::EmptyRawData);
}
if section.permissions.is_rwx() {
flags.push(EntropyFlag::Rwx);
}
if let Some(packer) =
detect_packer_by_section(&section.name)
{
if let Some(packer) = detect_packer_by_section(&section.name) {
flags.push(EntropyFlag::PackerSectionName);
packing_indicators.push(PackingIndicator {
indicator_type: "section_name".into(),
description: format!(
"Section name matches {packer} packer"
),
description: format!("Section name matches {packer} packer"),
evidence: section.name.clone(),
packer_name: Some(packer.into()),
});
@ -207,15 +183,11 @@ fn analyze_entropy(
}
}
if section.raw_size == 0
&& section.virtual_size > 0
&& section.permissions.execute
{
if section.raw_size == 0 && section.virtual_size > 0 && section.permissions.execute {
structural_count += 1;
packing_indicators.push(PackingIndicator {
indicator_type: "structural".into(),
description:
"Empty raw data with executable \
description: "Empty raw data with executable \
virtual section"
.into(),
evidence: format!(
@ -230,13 +202,8 @@ fn analyze_entropy(
structural_count += 1;
packing_indicators.push(PackingIndicator {
indicator_type: "structural".into(),
description:
"High virtual to raw size ratio"
.into(),
evidence: format!(
"section={} ratio={vr_ratio:.1}",
section.name
),
description: "High virtual to raw size ratio".into(),
evidence: format!("section={} ratio={vr_ratio:.1}", section.name),
packer_name: None,
});
}
@ -254,38 +221,27 @@ fn analyze_entropy(
});
}
if let Some(ep_section) = find_ep_section(
sections,
entry_point,
) {
if let Some(ep_section) = find_ep_section(sections, entry_point) {
let ep_file_offset = entry_point
.wrapping_sub(ep_section.virtual_address)
.wrapping_add(ep_section.raw_offset);
if let Some(&first_byte) =
data.get(ep_file_offset as usize)
if let Some(&first_byte) = data.get(ep_file_offset as usize)
&& first_byte == PUSHAD_OPCODE
{
if first_byte == PUSHAD_OPCODE {
packing_indicators.push(
PackingIndicator {
indicator_type: "entry_point"
.into(),
description:
"PUSHAD at entry point"
.into(),
packing_indicators.push(PackingIndicator {
indicator_type: "entry_point".into(),
description: "PUSHAD at entry point".into(),
evidence: format!(
"byte 0x{PUSHAD_OPCODE:02x} \
at EP offset 0x{ep_file_offset:x}"
),
packer_name: None,
},
);
}
});
}
}
let packing_detected = packer_name.is_some()
|| structural_count
>= STRUCTURAL_INDICATORS_FOR_PACKING;
let packing_detected =
packer_name.is_some() || structural_count >= STRUCTURAL_INDICATORS_FOR_PACKING;
EntropyResult {
overall_entropy,
@ -314,9 +270,7 @@ fn shannon_entropy(data: &[u8]) -> f64 {
.sum()
}
fn classify_entropy(
entropy: f64,
) -> EntropyClassification {
fn classify_entropy(entropy: f64) -> EntropyClassification {
if entropy < PLAINTEXT_MAX {
EntropyClassification::Plaintext
} else if entropy < NATIVE_CODE_MAX {
@ -330,20 +284,14 @@ fn classify_entropy(
}
}
fn detect_packer_by_section(
name: &str,
) -> Option<&'static str> {
fn detect_packer_by_section(name: &str) -> Option<&'static str> {
PACKER_SECTION_NAMES
.iter()
.find(|&&(section_name, _)| section_name == name)
.map(|&(_, packer)| packer)
}
fn read_section_data(
data: &[u8],
offset: u64,
size: u64,
) -> &[u8] {
fn read_section_data(data: &[u8], offset: u64, size: u64) -> &[u8] {
if size == 0 {
return &[];
}
@ -355,14 +303,9 @@ fn read_section_data(
&data[start..end]
}
fn find_ep_section(
sections: &[SectionInfo],
entry_point: u64,
) -> Option<&SectionInfo> {
fn find_ep_section(sections: &[SectionInfo], entry_point: u64) -> Option<&SectionInfo> {
sections.iter().find(|s| {
entry_point >= s.virtual_address
&& entry_point
< s.virtual_address + s.virtual_size
entry_point >= s.virtual_address && entry_point < s.virtual_address + s.virtual_size
})
}
@ -375,13 +318,8 @@ mod tests {
use crate::types::SectionPermissions;
fn load_fixture(name: &str) -> Vec<u8> {
let path = format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
}
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -402,8 +340,7 @@ mod tests {
#[test]
fn entropy_uniform_distribution() {
let data: Vec<u8> =
(0..=255u8).cycle().take(1024).collect();
let data: Vec<u8> = (0..=255u8).cycle().take(1024).collect();
let e = shannon_entropy(&data);
assert!(
(e - 8.0).abs() < 0.01,
@ -421,50 +358,20 @@ mod tests {
#[test]
fn entropy_classification_thresholds() {
assert_eq!(
classify_entropy(2.0),
EntropyClassification::Plaintext
);
assert_eq!(
classify_entropy(5.0),
EntropyClassification::NativeCode
);
assert_eq!(
classify_entropy(6.5),
EntropyClassification::Compressed
);
assert_eq!(
classify_entropy(7.1),
EntropyClassification::Packed
);
assert_eq!(
classify_entropy(7.5),
EntropyClassification::Encrypted
);
assert_eq!(classify_entropy(2.0), EntropyClassification::Plaintext);
assert_eq!(classify_entropy(5.0), EntropyClassification::NativeCode);
assert_eq!(classify_entropy(6.5), EntropyClassification::Compressed);
assert_eq!(classify_entropy(7.1), EntropyClassification::Packed);
assert_eq!(classify_entropy(7.5), EntropyClassification::Encrypted);
}
#[test]
fn packer_section_name_detection() {
assert_eq!(
detect_packer_by_section("UPX0"),
Some("UPX")
);
assert_eq!(
detect_packer_by_section("UPX1"),
Some("UPX")
);
assert_eq!(
detect_packer_by_section(".vmp0"),
Some("VMProtect")
);
assert_eq!(
detect_packer_by_section(".themida"),
Some("Themida")
);
assert_eq!(
detect_packer_by_section(".text"),
None
);
assert_eq!(detect_packer_by_section("UPX0"), Some("UPX"));
assert_eq!(detect_packer_by_section("UPX1"), Some("UPX"));
assert_eq!(detect_packer_by_section(".vmp0"), Some("VMProtect"));
assert_eq!(detect_packer_by_section(".themida"), Some("Themida"));
assert_eq!(detect_packer_by_section(".text"), None);
}
#[test]
@ -483,17 +390,11 @@ mod tests {
sha256: String::new(),
}];
let data: Vec<u8> =
(0..=255u8).cycle().take(256).collect();
let result =
analyze_entropy(&data, &sections, 0x1000);
let data: Vec<u8> = (0..=255u8).cycle().take(256).collect();
let result = analyze_entropy(&data, &sections, 0x1000);
let text_section = &result.sections[0];
assert!(
text_section.entropy > HIGH_ENTROPY_THRESHOLD
);
assert!(text_section
.flags
.contains(&EntropyFlag::HighEntropy));
assert!(text_section.entropy > HIGH_ENTROPY_THRESHOLD);
assert!(text_section.flags.contains(&EntropyFlag::HighEntropy));
assert!(text_section.is_anomalous);
}
@ -529,27 +430,17 @@ mod tests {
];
let data = vec![0u8; 0x4200];
let result =
analyze_entropy(&data, &sections, 0x11000);
let result = analyze_entropy(&data, &sections, 0x11000);
assert!(result.packing_detected);
assert_eq!(
result.packer_name,
Some("UPX".into())
);
assert_eq!(result.packer_name, Some("UPX".into()));
assert!(!result.packing_indicators.is_empty());
}
#[test]
fn elf_entropy_analysis() {
let data = load_fixture("hello_elf");
let format_result =
crate::formats::parse_format(&data)
.unwrap();
let result = analyze_entropy(
&data,
&format_result.sections,
format_result.entry_point,
);
let format_result = crate::formats::parse_format(&data).unwrap();
let result = analyze_entropy(&data, &format_result.sections, format_result.entry_point);
assert!(result.overall_entropy > 0.0);
assert!(!result.sections.is_empty());
@ -561,9 +452,7 @@ mod tests {
let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data);
crate::passes::format::FormatPass
.run(&mut ctx)
.unwrap();
crate::passes::format::FormatPass.run(&mut ctx).unwrap();
assert!(ctx.format_result.is_some());
EntropyPass.run(&mut ctx).unwrap();

View File

@ -36,12 +36,8 @@ impl AnalysisPass for FormatPass {
&[]
}
fn run(
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
let result =
formats::parse_format(ctx.data())?;
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
let result = formats::parse_format(ctx.data())?;
ctx.format_result = Some(result);
Ok(())
}
@ -56,13 +52,8 @@ mod tests {
use crate::types::{Architecture, BinaryFormat, Endianness};
fn load_fixture(name: &str) -> Vec<u8> {
let path = format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
}
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -78,19 +69,12 @@ mod tests {
#[test]
fn parse_elf_basic_metadata() {
let data = load_fixture("hello_elf");
let result =
formats::parse_format(&data).unwrap();
let result = formats::parse_format(&data).unwrap();
assert_eq!(result.format, BinaryFormat::Elf);
assert_eq!(
result.architecture,
Architecture::X86_64
);
assert_eq!(result.architecture, Architecture::X86_64);
assert_eq!(result.bits, 64);
assert_eq!(
result.endianness,
Endianness::Little
);
assert_eq!(result.endianness, Endianness::Little);
assert!(result.entry_point > 0);
assert!(result.is_pie);
assert!(!result.is_stripped);
@ -99,14 +83,10 @@ mod tests {
#[test]
fn parse_elf_sections_present() {
let data = load_fixture("hello_elf");
let result =
formats::parse_format(&data).unwrap();
let result = formats::parse_format(&data).unwrap();
assert!(!result.sections.is_empty());
let text = result
.sections
.iter()
.find(|s| s.name == ".text");
let text = result.sections.iter().find(|s| s.name == ".text");
assert!(text.is_some());
assert!(text.unwrap().permissions.execute);
}
@ -114,41 +94,35 @@ mod tests {
#[test]
fn parse_elf_segments_present() {
let data = load_fixture("hello_elf");
let result =
formats::parse_format(&data).unwrap();
let result = formats::parse_format(&data).unwrap();
assert!(!result.segments.is_empty());
let load_segments: Vec<_> = result
.segments
.iter()
.filter(|s| {
s.name.as_deref() == Some("LOAD")
})
.filter(|s| s.name.as_deref() == Some("LOAD"))
.collect();
assert!(!load_segments.is_empty());
}
#[test]
fn parse_elf_stripped_detection() {
let data =
load_fixture("hello_elf_stripped");
let result =
formats::parse_format(&data).unwrap();
let data = load_fixture("hello_elf_stripped");
let result = formats::parse_format(&data).unwrap();
assert!(result.is_stripped);
assert!(result.anomalies.iter().any(|a| {
matches!(
a,
FormatAnomaly::StrippedBinary
)
}));
assert!(
result
.anomalies
.iter()
.any(|a| { matches!(a, FormatAnomaly::StrippedBinary) })
);
}
#[test]
fn parse_elf_info_populated() {
let data = load_fixture("hello_elf");
let result =
formats::parse_format(&data).unwrap();
let result = formats::parse_format(&data).unwrap();
let elf_info = result.elf_info.unwrap();
assert!(!elf_info.os_abi.is_empty());
@ -160,18 +134,10 @@ mod tests {
#[test]
fn parse_elf_section_hashes() {
let data = load_fixture("hello_elf");
let result =
formats::parse_format(&data).unwrap();
let result = formats::parse_format(&data).unwrap();
let text = result
.sections
.iter()
.find(|s| s.name == ".text")
.unwrap();
assert!(
!text.sha256.is_empty(),
".text section should have a hash"
);
let text = result.sections.iter().find(|s| s.name == ".text").unwrap();
assert!(!text.sha256.is_empty(), ".text section should have a hash");
assert_eq!(text.sha256.len(), 64);
}
@ -184,8 +150,8 @@ mod tests {
#[test]
fn format_pass_populates_context() {
use crate::pass::AnalysisPass;
use super::FormatPass;
use crate::pass::AnalysisPass;
let data = load_fixture("hello_elf");
let mut ctx = make_ctx(data);

View File

@ -47,6 +47,8 @@ use crate::error::EngineError;
use crate::pass::{AnalysisPass, Sealed};
use crate::types::Severity;
type ImportsExportsLibs = (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>);
pub struct SuspiciousApiDef {
pub name: &'static str,
pub tag: &'static str,
@ -179,11 +181,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
name: "Process Injection Chain",
description: "VirtualAllocEx + WriteProcessMemory \
+ CreateRemoteThread",
patterns: &[
"VirtualAllocEx",
"WriteProcessMemory",
"CreateRemoteThread",
],
patterns: &["VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread"],
mitre_id: "T1055",
severity: Severity::Critical,
},
@ -211,50 +209,35 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
CombinationDef {
name: "DLL Injection",
description: "LoadLibrary + CreateRemoteThread",
patterns: &[
"LoadLibrary*",
"CreateRemoteThread",
],
patterns: &["LoadLibrary*", "CreateRemoteThread"],
mitre_id: "T1055.001",
severity: Severity::High,
},
CombinationDef {
name: "Credential Theft",
description: "OpenProcess + ReadProcessMemory",
patterns: &[
"OpenProcess",
"ReadProcessMemory",
],
patterns: &["OpenProcess", "ReadProcessMemory"],
mitre_id: "T1003",
severity: Severity::Critical,
},
CombinationDef {
name: "Service Persistence",
description: "OpenSCManager + CreateService",
patterns: &[
"OpenSCManager*",
"CreateService*",
],
patterns: &["OpenSCManager*", "CreateService*"],
mitre_id: "T1543.003",
severity: Severity::Medium,
},
CombinationDef {
name: "Registry Persistence",
description: "RegOpenKeyEx + RegSetValueEx",
patterns: &[
"RegOpenKeyEx*",
"RegSetValueEx*",
],
patterns: &["RegOpenKeyEx*", "RegSetValueEx*"],
mitre_id: "T1547.001",
severity: Severity::Medium,
},
CombinationDef {
name: "Download and Execute",
description: "URLDownloadToFile + ShellExecute",
patterns: &[
"URLDownloadToFile*",
"ShellExecute*",
],
patterns: &["URLDownloadToFile*", "ShellExecute*"],
mitre_id: "T1105",
severity: Severity::High,
},
@ -283,11 +266,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
name: "Linux C2 Connection",
description: "socket + connect + inet_pton \
hardcoded C2 address",
patterns: &[
"socket",
"connect",
"inet_pton",
],
patterns: &["socket", "connect", "inet_pton"],
mitre_id: "T1071",
severity: Severity::High,
},
@ -295,12 +274,7 @@ const SUSPICIOUS_COMBINATIONS: &[CombinationDef] = &[
name: "Linux Network Listener",
description: "socket + bind + listen + accept \
backdoor listener",
patterns: &[
"socket",
"bind",
"listen",
"accept",
],
patterns: &["socket", "bind", "listen", "accept"],
mitre_id: "T1571",
severity: Severity::High,
},
@ -327,8 +301,7 @@ pub struct ImportResult {
pub imports: Vec<ImportEntry>,
pub exports: Vec<ExportEntry>,
pub libraries: Vec<String>,
pub suspicious_combinations:
Vec<SuspiciousCombination>,
pub suspicious_combinations: Vec<SuspiciousCombination>,
pub mitre_mappings: Vec<MitreMapping>,
pub statistics: ImportStatistics,
}
@ -389,43 +362,28 @@ impl AnalysisPass for ImportPass {
&["format"]
}
fn run(
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
let result = analyze_imports(ctx.data())?;
ctx.import_result = Some(result);
Ok(())
}
}
fn analyze_imports(
data: &[u8],
) -> Result<ImportResult, EngineError> {
let object =
goblin::Object::parse(data).map_err(|e| {
EngineError::InvalidBinary {
fn analyze_imports(data: &[u8]) -> Result<ImportResult, EngineError> {
let object = goblin::Object::parse(data).map_err(|e| EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
let (imports, exports, libraries) = match &object {
goblin::Object::Elf(elf) => extract_elf(elf),
goblin::Object::PE(pe) => extract_pe(pe),
goblin::Object::Mach(mach) => {
extract_mach(mach, data)?
}
goblin::Object::Mach(mach) => extract_mach(mach, data)?,
_ => (Vec::new(), Vec::new(), Vec::new()),
};
let suspicious_combinations =
detect_combinations(&imports);
let mitre_mappings =
collect_mitre_mappings(&imports);
let suspicious_count = imports
.iter()
.filter(|i| i.is_suspicious)
.count();
let suspicious_combinations = detect_combinations(&imports);
let mitre_mappings = collect_mitre_mappings(&imports);
let suspicious_count = imports.iter().filter(|i| i.is_suspicious).count();
let statistics = ImportStatistics {
total_imports: imports.len(),
@ -444,29 +402,19 @@ fn analyze_imports(
})
}
fn extract_elf(
elf: &goblin::elf::Elf,
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
let libraries: Vec<String> = elf
.libraries
.iter()
.map(|s| s.to_string())
.collect();
fn extract_elf(elf: &goblin::elf::Elf) -> ImportsExportsLibs {
let libraries: Vec<String> = elf.libraries.iter().map(|s| s.to_string()).collect();
let mut imports = Vec::new();
for sym in elf.dynsyms.iter() {
if !sym.is_import() || sym.st_name == 0 {
continue;
}
let name = elf
.dynstrtab
.get_at(sym.st_name)
.unwrap_or("");
let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("");
if name.is_empty() {
continue;
}
let (is_suspicious, threat_tags) =
flag_suspicious(name);
let (is_suspicious, threat_tags) = flag_suspicious(name);
imports.push(ImportEntry {
library: String::new(),
function: name.to_string(),
@ -479,16 +427,10 @@ fn extract_elf(
let mut exports = Vec::new();
for sym in elf.dynsyms.iter() {
if sym.is_import()
|| sym.st_value == 0
|| sym.st_name == 0
{
if sym.is_import() || sym.st_value == 0 || sym.st_name == 0 {
continue;
}
let name = elf
.dynstrtab
.get_at(sym.st_name)
.unwrap_or("");
let name = elf.dynstrtab.get_at(sym.st_name).unwrap_or("");
if name.is_empty() {
continue;
}
@ -504,17 +446,14 @@ fn extract_elf(
(imports, exports, libraries)
}
fn extract_pe(
pe: &goblin::pe::PE,
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
fn extract_pe(pe: &goblin::pe::PE) -> ImportsExportsLibs {
let mut lib_set = HashSet::new();
let mut imports = Vec::new();
for import in &pe.imports {
let dll = import.dll.to_string();
lib_set.insert(dll.clone());
let (is_suspicious, threat_tags) =
flag_suspicious(&import.name);
let (is_suspicious, threat_tags) = flag_suspicious(&import.name);
imports.push(ImportEntry {
library: dll,
function: import.name.to_string(),
@ -528,16 +467,11 @@ fn extract_pe(
let mut exports = Vec::new();
for export in &pe.exports {
let is_forwarded = export.reexport.is_some();
let forward_target =
export.reexport.as_ref().map(|r| match r {
goblin::pe::export::Reexport::DLLName {
export: name,
lib,
} => format!("{lib}!{name}"),
goblin::pe::export::Reexport::DLLOrdinal {
ordinal,
lib,
} => format!("{lib}!#{ordinal}"),
let forward_target = export.reexport.as_ref().map(|r| match r {
goblin::pe::export::Reexport::DLLName { export: name, lib } => format!("{lib}!{name}"),
goblin::pe::export::Reexport::DLLOrdinal { ordinal, lib } => {
format!("{lib}!#{ordinal}")
}
});
exports.push(ExportEntry {
name: export.name.map(|s| s.to_string()),
@ -548,34 +482,20 @@ fn extract_pe(
});
}
let libraries: Vec<String> =
lib_set.into_iter().collect();
let libraries: Vec<String> = lib_set.into_iter().collect();
(imports, exports, libraries)
}
fn extract_mach(
mach: &goblin::mach::Mach,
data: &[u8],
) -> Result<
(Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>),
EngineError,
> {
fn extract_mach(mach: &goblin::mach::Mach, data: &[u8]) -> Result<ImportsExportsLibs, EngineError> {
match mach {
goblin::mach::Mach::Binary(macho) => {
Ok(extract_single_macho(macho))
}
goblin::mach::Mach::Binary(macho) => Ok(extract_single_macho(macho)),
goblin::mach::Mach::Fat(fat) => {
for arch in fat.iter_arches() {
let arch = arch.map_err(|e| {
EngineError::InvalidBinary {
if let Some(arch) = fat.iter_arches().next() {
let arch = arch.map_err(|e| EngineError::InvalidBinary {
reason: e.to_string(),
}
})?;
let macho = goblin::mach::MachO::parse(
data,
arch.offset as usize,
)
.map_err(|e| {
let macho =
goblin::mach::MachO::parse(data, arch.offset as usize).map_err(|e| {
EngineError::InvalidBinary {
reason: e.to_string(),
}
@ -587,14 +507,11 @@ fn extract_mach(
}
}
fn extract_single_macho(
macho: &goblin::mach::MachO,
) -> (Vec<ImportEntry>, Vec<ExportEntry>, Vec<String>) {
fn extract_single_macho(macho: &goblin::mach::MachO) -> ImportsExportsLibs {
let mut imports = Vec::new();
if let Ok(macho_imports) = macho.imports() {
for imp in &macho_imports {
let (is_suspicious, threat_tags) =
flag_suspicious(imp.name);
let (is_suspicious, threat_tags) = flag_suspicious(imp.name);
imports.push(ImportEntry {
library: imp.dylib.to_string(),
function: imp.name.to_string(),
@ -629,9 +546,7 @@ fn extract_single_macho(
(imports, exports, libraries)
}
fn flag_suspicious(
name: &str,
) -> (bool, Vec<String>) {
fn flag_suspicious(name: &str) -> (bool, Vec<String>) {
let mut tags = Vec::new();
for api in SUSPICIOUS_APIS {
if matches_api(name, api.name) {
@ -642,24 +557,17 @@ fn flag_suspicious(
(is_suspicious, tags)
}
fn matches_api(
import_name: &str,
api_name: &str,
) -> bool {
fn matches_api(import_name: &str, api_name: &str) -> bool {
if import_name == api_name {
return true;
}
if import_name.starts_with(api_name) {
let suffix = &import_name[api_name.len()..];
if let Some(suffix) = import_name.strip_prefix(api_name) {
return suffix == "A" || suffix == "W";
}
false
}
fn matches_pattern(
import_name: &str,
pattern: &str,
) -> bool {
fn matches_pattern(import_name: &str, pattern: &str) -> bool {
if let Some(prefix) = pattern.strip_suffix('*') {
import_name.starts_with(prefix)
} else {
@ -667,13 +575,8 @@ fn matches_pattern(
}
}
fn detect_combinations(
imports: &[ImportEntry],
) -> Vec<SuspiciousCombination> {
let function_names: Vec<&str> = imports
.iter()
.map(|i| i.function.as_str())
.collect();
fn detect_combinations(imports: &[ImportEntry]) -> Vec<SuspiciousCombination> {
let function_names: Vec<&str> = imports.iter().map(|i| i.function.as_str()).collect();
let mut results = Vec::new();
let mut seen = HashSet::new();
@ -681,11 +584,10 @@ fn detect_combinations(
if seen.contains(combo.name) {
continue;
}
let all_matched =
combo.patterns.iter().all(|pattern| {
function_names.iter().any(|name| {
matches_pattern(name, pattern)
})
let all_matched = combo.patterns.iter().all(|pattern| {
function_names
.iter()
.any(|name| matches_pattern(name, pattern))
});
if !all_matched {
continue;
@ -697,9 +599,7 @@ fn detect_combinations(
.filter_map(|pattern| {
function_names
.iter()
.find(|name| {
matches_pattern(name, pattern)
})
.find(|name| matches_pattern(name, pattern))
.map(|name| name.to_string())
})
.collect();
@ -717,18 +617,14 @@ fn detect_combinations(
results
}
fn collect_mitre_mappings(
imports: &[ImportEntry],
) -> Vec<MitreMapping> {
fn collect_mitre_mappings(imports: &[ImportEntry]) -> Vec<MitreMapping> {
let mut mappings = Vec::new();
for import in imports {
if !import.is_suspicious {
continue;
}
for api in SUSPICIOUS_APIS {
if matches_api(&import.function, api.name)
&& !api.mitre_id.is_empty()
{
if matches_api(&import.function, api.name) && !api.mitre_id.is_empty() {
mappings.push(MitreMapping {
technique_id: api.mitre_id.into(),
api: import.function.clone(),
@ -748,13 +644,8 @@ mod tests {
use crate::context::BinarySource;
fn load_fixture(name: &str) -> Vec<u8> {
let path = format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
}
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -770,13 +661,9 @@ mod tests {
#[test]
fn elf_imports_extracted() {
let data = load_fixture("hello_elf");
let result =
analyze_imports(&data).unwrap();
let result = analyze_imports(&data).unwrap();
assert!(
!result.imports.is_empty(),
"ELF binary should have imports"
);
assert!(!result.imports.is_empty(), "ELF binary should have imports");
assert!(
!result.libraries.is_empty(),
"ELF binary should list needed libraries"
@ -786,20 +673,15 @@ mod tests {
#[test]
fn suspicious_api_flagging() {
let (is_suspicious, tags) =
flag_suspicious("VirtualAllocEx");
let (is_suspicious, tags) = flag_suspicious("VirtualAllocEx");
assert!(is_suspicious);
assert!(tags.contains(&"injection".to_string()));
let (is_suspicious, tags) =
flag_suspicious("RegSetValueExW");
let (is_suspicious, tags) = flag_suspicious("RegSetValueExW");
assert!(is_suspicious);
assert!(tags.contains(
&"persistence".to_string()
));
assert!(tags.contains(&"persistence".to_string()));
let (is_suspicious, _) =
flag_suspicious("printf");
let (is_suspicious, _) = flag_suspicious("printf");
assert!(!is_suspicious);
}
@ -812,9 +694,7 @@ mod tests {
address: None,
ordinal: None,
is_suspicious: true,
threat_tags: vec![
"injection".into(),
],
threat_tags: vec!["injection".into()],
},
ImportEntry {
library: "kernel32.dll".into(),
@ -822,9 +702,7 @@ mod tests {
address: None,
ordinal: None,
is_suspicious: true,
threat_tags: vec![
"injection".into(),
],
threat_tags: vec!["injection".into()],
},
ImportEntry {
library: "kernel32.dll".into(),
@ -832,23 +710,15 @@ mod tests {
address: None,
ordinal: None,
is_suspicious: true,
threat_tags: vec![
"injection".into(),
],
threat_tags: vec!["injection".into()],
},
];
let combos = detect_combinations(&imports);
assert_eq!(combos.len(), 1);
assert_eq!(
combos[0].name,
"Process Injection Chain"
);
assert_eq!(combos[0].name, "Process Injection Chain");
assert_eq!(combos[0].mitre_id, "T1055");
assert_eq!(
combos[0].severity,
Severity::Critical
);
assert_eq!(combos[0].severity, Severity::Critical);
}
#[test]
@ -868,17 +738,12 @@ mod tests {
address: None,
ordinal: None,
is_suspicious: true,
threat_tags: vec![
"persistence".into(),
],
threat_tags: vec!["persistence".into()],
},
];
let combos = detect_combinations(&imports);
assert!(combos
.iter()
.any(|c| c.name
== "Registry Persistence"));
assert!(combos.iter().any(|c| c.name == "Registry Persistence"));
}
#[test]
@ -894,8 +759,7 @@ mod tests {
let combos = detect_combinations(&imports);
assert!(
!combos.iter().any(|c| c.name
== "Process Injection Chain"),
!combos.iter().any(|c| c.name == "Process Injection Chain"),
"should not detect chain with only one API"
);
}
@ -909,9 +773,7 @@ mod tests {
address: None,
ordinal: None,
is_suspicious: true,
threat_tags: vec![
"injection".into(),
],
threat_tags: vec!["injection".into()],
},
ImportEntry {
library: String::new(),
@ -923,13 +785,9 @@ mod tests {
},
];
let mappings =
collect_mitre_mappings(&imports);
let mappings = collect_mitre_mappings(&imports);
assert_eq!(mappings.len(), 1);
assert_eq!(
mappings[0].technique_id,
"T1055.008"
);
assert_eq!(mappings[0].technique_id, "T1055.008");
assert_eq!(mappings[0].api, "ptrace");
}

View File

@ -70,23 +70,14 @@ const SUSPICIOUS_CATEGORIES: &[StringCategory] = &[
StringCategory::CryptoWallet,
];
const URL_PREFIXES: &[&str] =
&["http://", "https://", "ftp://"];
const URL_PREFIXES: &[&str] = &["http://", "https://", "ftp://"];
const UNIX_PATH_PREFIXES: &[&str] = &[
"/etc/", "/tmp/", "/var/", "/bin/", "/usr/",
"/dev/", "/proc/", "/sys/", "/opt/", "/home/",
"/etc/", "/tmp/", "/var/", "/bin/", "/usr/", "/dev/", "/proc/", "/sys/", "/opt/", "/home/",
"/root/", "/lib/", "/sbin/",
];
const REGISTRY_PREFIXES: &[&str] = &[
"HKEY_",
"HKLM\\",
"HKCU\\",
"HKCR\\",
"HKCC\\",
"HKU\\",
];
const REGISTRY_PREFIXES: &[&str] = &["HKEY_", "HKLM\\", "HKCU\\", "HKCR\\", "HKCC\\", "HKU\\"];
const SHELL_INDICATORS: &[&str] = &[
"cmd.exe",
@ -108,8 +99,7 @@ const SHELL_INDICATORS: &[&str] = &[
];
const PACKER_SIGNATURES: &[&str] = &[
"UPX!", "MPRESS", ".themida", ".vmp", ".enigma",
"PEC2", "ASPack", "MEW ",
"UPX!", "MPRESS", ".themida", ".vmp", ".enigma", "PEC2", "ASPack", "MEW ",
];
const DEBUG_ARTIFACTS: &[&str] = &[
@ -163,8 +153,7 @@ const PERSISTENCE_PATHS: &[&str] = &[
".config/autostart",
];
const BASE64_CHARS: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const BASE64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const BASE64_MIN_LENGTH: usize = 20;
@ -215,25 +204,15 @@ impl AnalysisPass for StringPass {
&["format"]
}
fn run(
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
let sections = ctx
.format_result
.as_ref()
.map(|fr| fr.sections.as_slice());
let result =
extract_strings(ctx.data(), sections);
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError> {
let sections = ctx.format_result.as_ref().map(|fr| fr.sections.as_slice());
let result = extract_strings(ctx.data(), sections);
ctx.string_result = Some(result);
Ok(())
}
}
fn extract_strings(
data: &[u8],
sections: Option<&[SectionInfo]>,
) -> StringResult {
fn extract_strings(data: &[u8], sections: Option<&[SectionInfo]>) -> StringResult {
let mut strings = Vec::new();
extract_ascii(data, sections, &mut strings);
@ -244,12 +223,8 @@ fn extract_strings(
let mut suspicious_count = 0;
for s in &strings {
*by_encoding
.entry(s.encoding.clone())
.or_insert(0) += 1;
*by_category
.entry(s.category.clone())
.or_insert(0) += 1;
*by_encoding.entry(s.encoding.clone()).or_insert(0) += 1;
*by_category.entry(s.category.clone()).or_insert(0) += 1;
if s.is_suspicious {
suspicious_count += 1;
}
@ -270,17 +245,10 @@ fn extract_strings(
}
fn is_printable(b: u8) -> bool {
(PRINTABLE_MIN..=PRINTABLE_MAX).contains(&b)
|| b == TAB
|| b == NEWLINE
|| b == CARRIAGE_RETURN
(PRINTABLE_MIN..=PRINTABLE_MAX).contains(&b) || b == TAB || b == NEWLINE || b == CARRIAGE_RETURN
}
fn extract_ascii(
data: &[u8],
sections: Option<&[SectionInfo]>,
out: &mut Vec<ExtractedString>,
) {
fn extract_ascii(data: &[u8], sections: Option<&[SectionInfo]>, out: &mut Vec<ExtractedString>) {
let mut start = 0;
let mut in_run = false;
@ -292,27 +260,18 @@ fn extract_ascii(
}
} else if in_run {
let len = i - start;
if len >= MIN_STRING_LENGTH {
if let Ok(s) =
std::str::from_utf8(&data[start..i])
if len >= MIN_STRING_LENGTH
&& let Ok(s) = std::str::from_utf8(&data[start..i])
{
let is_multibyte = s
.bytes()
.any(|b| !b.is_ascii());
let is_multibyte = s.bytes().any(|b| !b.is_ascii());
let encoding = if is_multibyte {
StringEncoding::Utf8
} else {
StringEncoding::Ascii
};
let category = classify(s);
let is_suspicious =
is_category_suspicious(&category);
let section = sections.and_then(|secs| {
find_section(
secs,
start as u64,
)
});
let is_suspicious = is_category_suspicious(&category);
let section = sections.and_then(|secs| find_section(secs, start as u64));
out.push(ExtractedString {
value: s.to_string(),
offset: start as u64,
@ -323,30 +282,24 @@ fn extract_ascii(
section,
});
}
}
in_run = false;
}
}
if in_run {
let len = data.len() - start;
if len >= MIN_STRING_LENGTH {
if let Ok(s) =
std::str::from_utf8(&data[start..])
if len >= MIN_STRING_LENGTH
&& let Ok(s) = std::str::from_utf8(&data[start..])
{
let is_multibyte =
s.bytes().any(|b| !b.is_ascii());
let is_multibyte = s.bytes().any(|b| !b.is_ascii());
let encoding = if is_multibyte {
StringEncoding::Utf8
} else {
StringEncoding::Ascii
};
let category = classify(s);
let is_suspicious =
is_category_suspicious(&category);
let section = sections.and_then(|secs| {
find_section(secs, start as u64)
});
let is_suspicious = is_category_suspicious(&category);
let section = sections.and_then(|secs| find_section(secs, start as u64));
out.push(ExtractedString {
value: s.to_string(),
offset: start as u64,
@ -359,13 +312,8 @@ fn extract_ascii(
}
}
}
}
fn extract_utf16le(
data: &[u8],
sections: Option<&[SectionInfo]>,
out: &mut Vec<ExtractedString>,
) {
fn extract_utf16le(data: &[u8], sections: Option<&[SectionInfo]>, out: &mut Vec<ExtractedString>) {
let mut i = 0;
while i + 1 < data.len() {
let lo = data[i];
@ -391,14 +339,10 @@ fn extract_utf16le(
}
if code_units.len() >= MIN_STRING_LENGTH {
let value =
String::from_utf16_lossy(&code_units);
let value = String::from_utf16_lossy(&code_units);
let category = classify(&value);
let is_suspicious =
is_category_suspicious(&category);
let section = sections.and_then(|secs| {
find_section(secs, start as u64)
});
let is_suspicious = is_category_suspicious(&category);
let section = sections.and_then(|secs| find_section(secs, start as u64));
out.push(ExtractedString {
value,
offset: start as u64,
@ -417,17 +361,11 @@ fn extract_utf16le(
}
}
fn find_section(
sections: &[SectionInfo],
file_offset: u64,
) -> Option<String> {
fn find_section(sections: &[SectionInfo], file_offset: u64) -> Option<String> {
sections
.iter()
.find(|s| {
s.raw_size > 0
&& file_offset >= s.raw_offset
&& file_offset
< s.raw_offset + s.raw_size
s.raw_size > 0 && file_offset >= s.raw_offset && file_offset < s.raw_offset + s.raw_size
})
.map(|s| s.name.clone())
}
@ -475,17 +413,13 @@ fn classify(s: &str) -> StringCategory {
StringCategory::Generic
}
fn is_category_suspicious(
category: &StringCategory,
) -> bool {
fn is_category_suspicious(category: &StringCategory) -> bool {
SUSPICIOUS_CATEGORIES.contains(category)
}
fn is_url(s: &str) -> bool {
let lower = s.to_ascii_lowercase();
URL_PREFIXES
.iter()
.any(|prefix| lower.starts_with(prefix))
URL_PREFIXES.iter().any(|prefix| lower.starts_with(prefix))
}
fn is_ip_address(s: &str) -> bool {
@ -521,9 +455,7 @@ fn is_file_path(s: &str) -> bool {
}
fn is_registry_key(s: &str) -> bool {
REGISTRY_PREFIXES
.iter()
.any(|prefix| s.starts_with(prefix))
REGISTRY_PREFIXES.iter().any(|prefix| s.starts_with(prefix))
}
fn is_shell_command(s: &str) -> bool {
@ -537,11 +469,10 @@ fn is_crypto_wallet(s: &str) -> bool {
if s.len() >= BTC_MIN_LENGTH
&& s.len() <= BTC_MAX_LENGTH
&& (s.starts_with('1') || s.starts_with('3'))
&& s.chars()
.all(|c| c.is_ascii_alphanumeric())
&& !s.chars().any(|c| {
c == '0' || c == 'O' || c == 'I' || c == 'l'
})
&& s.chars().all(|c| c.is_ascii_alphanumeric())
&& !s
.chars()
.any(|c| c == '0' || c == 'O' || c == 'I' || c == 'l')
{
return true;
}
@ -572,51 +503,35 @@ fn is_email(s: &str) -> bool {
return false;
}
let local = &s[..at_pos];
local
local.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '%' || c == '+' || c == '-'
}) && domain[..dot_pos]
.chars()
.all(|c| c.is_ascii_alphanumeric()
|| c == '.'
|| c == '_'
|| c == '%'
|| c == '+'
|| c == '-')
&& domain[..dot_pos]
.chars()
.all(|c| c.is_ascii_alphanumeric()
|| c == '.'
|| c == '-')
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
&& tld.chars().all(|c| c.is_ascii_alphabetic())
}
fn is_suspicious_api(s: &str) -> bool {
SUSPICIOUS_APIS
.iter()
.any(|api| s == api.name)
SUSPICIOUS_APIS.iter().any(|api| s == api.name)
}
fn is_packer_signature(s: &str) -> bool {
PACKER_SIGNATURES
.iter()
.any(|sig| s.contains(sig))
PACKER_SIGNATURES.iter().any(|sig| s.contains(sig))
}
fn is_debug_artifact(s: &str) -> bool {
DEBUG_ARTIFACTS
.iter()
.any(|artifact| s.contains(artifact))
DEBUG_ARTIFACTS.iter().any(|artifact| s.contains(artifact))
}
fn is_anti_analysis(s: &str) -> bool {
let lower = s.to_ascii_lowercase();
ANTI_ANALYSIS_INDICATORS.iter().any(|indicator| {
lower.contains(&indicator.to_ascii_lowercase())
})
ANTI_ANALYSIS_INDICATORS
.iter()
.any(|indicator| lower.contains(&indicator.to_ascii_lowercase()))
}
fn is_persistence_path(s: &str) -> bool {
PERSISTENCE_PATHS
.iter()
.any(|path| s.contains(path))
PERSISTENCE_PATHS.iter().any(|path| s.contains(path))
}
fn is_encoded_data(s: &str) -> bool {
@ -627,9 +542,7 @@ fn is_encoded_data(s: &str) -> bool {
if trimmed.is_empty() {
return false;
}
let all_base64 = trimmed
.bytes()
.all(|b| BASE64_CHARS.contains(&b));
let all_base64 = trimmed.bytes().all(|b| BASE64_CHARS.contains(&b));
if !all_base64 {
return false;
}
@ -645,13 +558,8 @@ mod tests {
use crate::context::BinarySource;
fn load_fixture(name: &str) -> Vec<u8> {
let path = format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
}
fn make_ctx(data: Vec<u8>) -> AnalysisContext {
@ -666,23 +574,12 @@ mod tests {
#[test]
fn ascii_extraction_min_length() {
let data =
b"abc\x00abcdef\x00ab\x00longstring\x00";
let data = b"abc\x00abcdef\x00ab\x00longstring\x00";
let result = extract_strings(data, None);
let values: Vec<&str> = result
.strings
.iter()
.map(|s| s.value.as_str())
.collect();
assert!(
!values.contains(&"abc"),
"3-char string should be excluded"
);
assert!(
!values.contains(&"ab"),
"2-char string should be excluded"
);
let values: Vec<&str> = result.strings.iter().map(|s| s.value.as_str()).collect();
assert!(!values.contains(&"abc"), "3-char string should be excluded");
assert!(!values.contains(&"ab"), "2-char string should be excluded");
assert!(
values.contains(&"abcdef"),
"6-char string should be included"
@ -704,57 +601,28 @@ mod tests {
data.push(0x00);
let result = extract_strings(&data, None);
let utf16_strings: Vec<&ExtractedString> =
result
let utf16_strings: Vec<&ExtractedString> = result
.strings
.iter()
.filter(|s| {
s.encoding == StringEncoding::Utf16Le
})
.filter(|s| s.encoding == StringEncoding::Utf16Le)
.collect();
assert!(
!utf16_strings.is_empty(),
"should extract UTF-16LE string"
);
assert!(utf16_strings
.iter()
.any(|s| s.value.contains("Hello")));
assert!(!utf16_strings.is_empty(), "should extract UTF-16LE string");
assert!(utf16_strings.iter().any(|s| s.value.contains("Hello")));
}
#[test]
fn category_url() {
assert_eq!(
classify("https://evil.com/payload"),
StringCategory::Url,
);
assert_eq!(
classify("http://malware.ru/dropper"),
StringCategory::Url,
);
assert_eq!(
classify("ftp://files.example.com"),
StringCategory::Url,
);
assert_eq!(classify("https://evil.com/payload"), StringCategory::Url,);
assert_eq!(classify("http://malware.ru/dropper"), StringCategory::Url,);
assert_eq!(classify("ftp://files.example.com"), StringCategory::Url,);
}
#[test]
fn category_ip_address() {
assert_eq!(
classify("192.168.1.1"),
StringCategory::IpAddress,
);
assert_eq!(
classify("10.0.0.1"),
StringCategory::IpAddress,
);
assert_ne!(
classify("999.999.999.999"),
StringCategory::IpAddress,
);
assert_ne!(
classify("1.2.3"),
StringCategory::IpAddress,
);
assert_eq!(classify("192.168.1.1"), StringCategory::IpAddress,);
assert_eq!(classify("10.0.0.1"), StringCategory::IpAddress,);
assert_ne!(classify("999.999.999.999"), StringCategory::IpAddress,);
assert_ne!(classify("1.2.3"), StringCategory::IpAddress,);
}
#[test]
@ -763,10 +631,7 @@ mod tests {
classify("HKLM\\Software\\Microsoft"),
StringCategory::RegistryKey,
);
assert_eq!(
classify("HKEY_LOCAL_MACHINE"),
StringCategory::RegistryKey,
);
assert_eq!(classify("HKEY_LOCAL_MACHINE"), StringCategory::RegistryKey,);
}
#[test]
@ -775,22 +640,13 @@ mod tests {
classify("C:\\Windows\\System32\\notepad.exe"),
StringCategory::FilePath,
);
assert_eq!(
classify("/tmp/output.log"),
StringCategory::FilePath,
);
assert_eq!(
classify("\\\\server\\share"),
StringCategory::FilePath,
);
assert_eq!(classify("/tmp/output.log"), StringCategory::FilePath,);
assert_eq!(classify("\\\\server\\share"), StringCategory::FilePath,);
}
#[test]
fn category_shell_command() {
assert_eq!(
classify("cmd.exe /c whoami"),
StringCategory::ShellCommand,
);
assert_eq!(classify("cmd.exe /c whoami"), StringCategory::ShellCommand,);
assert_eq!(
classify("/bin/bash -c echo hi"),
StringCategory::ShellCommand,
@ -799,10 +655,7 @@ mod tests {
#[test]
fn category_packer_signature() {
assert_eq!(
classify("UPX!"),
StringCategory::PackerSignature,
);
assert_eq!(classify("UPX!"), StringCategory::PackerSignature,);
assert_eq!(
classify("This is .themida packed"),
StringCategory::PackerSignature,
@ -815,10 +668,7 @@ mod tests {
classify("VMware Virtual Platform"),
StringCategory::AntiAnalysis,
);
assert_eq!(
classify("wireshark.exe"),
StringCategory::AntiAnalysis,
);
assert_eq!(classify("wireshark.exe"), StringCategory::AntiAnalysis,);
}
#[test]
@ -830,63 +680,34 @@ mod tests {
),
StringCategory::PersistencePath,
);
assert_eq!(
classify("/etc/crontab"),
StringCategory::PersistencePath,
);
assert_eq!(classify("/etc/crontab"), StringCategory::PersistencePath,);
}
#[test]
fn category_encoded_data() {
assert_eq!(
classify(
"SGVsbG8gV29ybGQhIFRoaXMgaXM="
),
StringCategory::EncodedData,
);
assert_ne!(
classify("short"),
classify("SGVsbG8gV29ybGQhIFRoaXMgaXM="),
StringCategory::EncodedData,
);
assert_ne!(classify("short"), StringCategory::EncodedData,);
}
#[test]
fn category_email() {
assert_eq!(
classify("user@example.com"),
StringCategory::Email,
);
assert_eq!(classify("user@example.com"), StringCategory::Email,);
}
#[test]
fn suspicious_flag_by_category() {
assert!(is_category_suspicious(
&StringCategory::ShellCommand
));
assert!(is_category_suspicious(
&StringCategory::AntiAnalysis
));
assert!(is_category_suspicious(
&StringCategory::PackerSignature
));
assert!(is_category_suspicious(
&StringCategory::PersistencePath
));
assert!(is_category_suspicious(
&StringCategory::EncodedData
));
assert!(is_category_suspicious(
&StringCategory::CryptoWallet
));
assert!(is_category_suspicious(
&StringCategory::SuspiciousApi
));
assert!(!is_category_suspicious(
&StringCategory::Generic
));
assert!(!is_category_suspicious(
&StringCategory::Url
));
assert!(is_category_suspicious(&StringCategory::ShellCommand));
assert!(is_category_suspicious(&StringCategory::AntiAnalysis));
assert!(is_category_suspicious(&StringCategory::PackerSignature));
assert!(is_category_suspicious(&StringCategory::PersistencePath));
assert!(is_category_suspicious(&StringCategory::EncodedData));
assert!(is_category_suspicious(&StringCategory::CryptoWallet));
assert!(is_category_suspicious(&StringCategory::SuspiciousApi));
assert!(!is_category_suspicious(&StringCategory::Generic));
assert!(!is_category_suspicious(&StringCategory::Url));
}
#[test]
@ -919,8 +740,7 @@ mod tests {
virtual_size: 100,
raw_offset: 10,
raw_size: 100,
permissions:
crate::types::SectionPermissions {
permissions: crate::types::SectionPermissions {
read: true,
write: false,
execute: false,

View File

@ -271,57 +271,37 @@ pub struct YaraScanner {
impl YaraScanner {
pub fn new() -> Result<Self, EngineError> {
let mut compiler = yara_x::Compiler::new();
compiler.add_source(BUILTIN_RULES).map_err(
|e| EngineError::Yara(e.to_string()),
)?;
compiler
.add_source(BUILTIN_RULES)
.map_err(|e| EngineError::Yara(e.to_string()))?;
Ok(Self {
rules: compiler.build(),
})
}
pub fn with_custom_rules(
rules_dir: &Path,
) -> Result<Self, EngineError> {
pub fn with_custom_rules(rules_dir: &Path) -> Result<Self, EngineError> {
let mut compiler = yara_x::Compiler::new();
compiler.add_source(BUILTIN_RULES).map_err(
|e| EngineError::Yara(e.to_string()),
)?;
compiler
.add_source(BUILTIN_RULES)
.map_err(|e| EngineError::Yara(e.to_string()))?;
if rules_dir.is_dir() {
for entry in std::fs::read_dir(rules_dir)
.map_err(|e| {
EngineError::Yara(format!(
"failed to read rules dir: {e}"
))
})?
.map_err(|e| EngineError::Yara(format!("failed to read rules dir: {e}")))?
{
let entry = entry.map_err(|e| {
EngineError::Yara(format!(
"dir entry error: {e}"
))
})?;
let entry =
entry.map_err(|e| EngineError::Yara(format!("dir entry error: {e}")))?;
let path = entry.path();
if path
.extension()
.is_some_and(|ext| ext == "yar" || ext == "yara")
{
let source = std::fs::read_to_string(
&path,
)
.map_err(|e| {
EngineError::Yara(format!(
"failed to read {}: {e}",
path.display()
))
let source = std::fs::read_to_string(&path).map_err(|e| {
EngineError::Yara(format!("failed to read {}: {e}", path.display()))
})?;
compiler
.add_source(source.as_str())
.map_err(|e| {
EngineError::Yara(format!(
"compile error in {}: {e}",
path.display()
))
compiler.add_source(source.as_str()).map_err(|e| {
EngineError::Yara(format!("compile error in {}: {e}", path.display()))
})?;
}
}
@ -332,22 +312,15 @@ impl YaraScanner {
})
}
pub fn scan(
&self,
data: &[u8],
) -> Result<Vec<YaraMatch>, EngineError> {
let mut scanner =
yara_x::Scanner::new(&self.rules);
let results = scanner.scan(data).map_err(
|e| EngineError::Yara(e.to_string()),
)?;
pub fn scan(&self, data: &[u8]) -> Result<Vec<YaraMatch>, EngineError> {
let mut scanner = yara_x::Scanner::new(&self.rules);
let results = scanner
.scan(data)
.map_err(|e| EngineError::Yara(e.to_string()))?;
let mut matches = Vec::new();
for rule in results.matching_rules() {
let tags: Vec<String> = rule
.tags()
.map(|t| t.identifier().to_string())
.collect();
let tags: Vec<String> = rule.tags().map(|t| t.identifier().to_string()).collect();
let mut description = None;
let mut category = None;
@ -356,20 +329,17 @@ impl YaraScanner {
match key {
"description" => {
if let yara_x::MetaValue::String(s) = value {
description =
Some(s.to_string());
description = Some(s.to_string());
}
}
"category" => {
if let yara_x::MetaValue::String(s) = value {
category =
Some(s.to_string());
category = Some(s.to_string());
}
}
"severity" => {
if let yara_x::MetaValue::String(s) = value {
severity =
Some(s.to_string());
severity = Some(s.to_string());
}
}
_ => {}
@ -378,25 +348,18 @@ impl YaraScanner {
let mut matched_strings = Vec::new();
for pattern in rule.patterns() {
let id = pattern
.identifier()
.to_string();
let count =
pattern.matches().count();
let id = pattern.identifier().to_string();
let count = pattern.matches().count();
if count > 0 {
matched_strings.push(
YaraStringMatch {
matched_strings.push(YaraStringMatch {
identifier: id,
match_count: count,
},
);
});
}
}
matches.push(YaraMatch {
rule_name: rule
.identifier()
.to_string(),
rule_name: rule.identifier().to_string(),
tags,
metadata: YaraMetadata {
description,
@ -435,42 +398,25 @@ mod tests {
let upx_match = result
.iter()
.find(|m| m.rule_name == "suspicious_upx_packed");
assert!(
upx_match.is_some(),
"should detect UPX packer signature"
);
let meta =
&upx_match.unwrap().metadata;
assert_eq!(
meta.category.as_deref(),
Some("packer")
);
assert!(upx_match.is_some(), "should detect UPX packer signature");
let meta = &upx_match.unwrap().metadata;
assert_eq!(meta.category.as_deref(), Some("packer"));
}
#[test]
fn detects_process_injection() {
let mut data = Vec::new();
data.extend_from_slice(
b"\x00\x00VirtualAllocEx\x00\x00",
);
data.extend_from_slice(
b"\x00\x00WriteProcessMemory\x00\x00",
);
data.extend_from_slice(
b"\x00\x00CreateRemoteThread\x00\x00",
);
data.extend_from_slice(b"\x00\x00VirtualAllocEx\x00\x00");
data.extend_from_slice(b"\x00\x00WriteProcessMemory\x00\x00");
data.extend_from_slice(b"\x00\x00CreateRemoteThread\x00\x00");
data.extend_from_slice(&[0u8; 256]);
let scanner = YaraScanner::new().unwrap();
let result = scanner.scan(&data).unwrap();
let injection = result.iter().find(|m| {
m.rule_name
== "suspicious_process_injection"
});
assert!(
injection.is_some(),
"should detect process injection APIs"
);
let injection = result
.iter()
.find(|m| m.rule_name == "suspicious_process_injection");
assert!(injection.is_some(), "should detect process injection APIs");
}
#[test]
@ -480,9 +426,7 @@ mod tests {
let result = scanner.scan(data).unwrap();
let suspicious: Vec<_> = result
.iter()
.filter(|m| {
m.rule_name != "suspicious_obfuscation"
})
.filter(|m| m.rule_name != "suspicious_obfuscation")
.collect();
assert!(
suspicious.is_empty(),
@ -492,13 +436,8 @@ mod tests {
}
fn load_fixture(name: &str) -> Vec<u8> {
let path = format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("fixture {path}: {e}")
})
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
}
#[test]

View File

@ -20,24 +20,19 @@
// lib.rs - AnalysisEngine
// types.rs - BinaryFormat
use axumortem_engine::types::BinaryFormat;
use axumortem_engine::AnalysisEngine;
use axumortem_engine::types::BinaryFormat;
fn load_fixture(name: &str) -> Vec<u8> {
let path = format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR"),
);
std::fs::read(&path)
.unwrap_or_else(|e| panic!("fixture {path}: {e}"))
let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"),);
std::fs::read(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"))
}
#[test]
fn full_pipeline_elf() {
let engine = AnalysisEngine::new().unwrap();
let data = load_fixture("hello_elf");
let (ctx, report) =
engine.analyze(&data, "hello_elf");
let (ctx, report) = engine.analyze(&data, "hello_elf");
assert!(
report.all_succeeded(),
@ -58,8 +53,7 @@ fn full_pipeline_elf() {
assert!(ctx.entropy_result.is_some());
assert!(ctx.disassembly_result.is_some());
let disasm =
ctx.disassembly_result.as_ref().unwrap();
let disasm = ctx.disassembly_result.as_ref().unwrap();
assert!(disasm.total_functions > 0);
assert!(disasm.total_instructions > 0);
@ -73,8 +67,7 @@ fn full_pipeline_elf() {
fn full_pipeline_stripped_elf() {
let engine = AnalysisEngine::new().unwrap();
let data = load_fixture("hello_elf_stripped");
let (ctx, report) =
engine.analyze(&data, "hello_elf_stripped");
let (ctx, report) = engine.analyze(&data, "hello_elf_stripped");
assert!(report.all_succeeded());
@ -98,15 +91,11 @@ fn sha256_computed_correctly() {
fn invalid_binary_handled() {
let engine = AnalysisEngine::new().unwrap();
let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
let (_, report) =
engine.analyze(&data, "garbage.bin");
let (_, report) = engine.analyze(&data, "garbage.bin");
assert!(
!report.all_succeeded(),
"invalid binary should cause format pass failure"
);
assert!(report
.failed_passes()
.iter()
.any(|p| p.name == "format"));
assert!(report.failed_passes().iter().any(|p| p.name == "format"));
}

View File

@ -17,8 +17,6 @@ pub mod queries;
use sqlx::PgPool;
pub async fn run_migrations(
pool: &PgPool,
) -> Result<(), sqlx::migrate::MigrateError> {
pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
sqlx::migrate!("./migrations").run(pool).await
}

View File

@ -38,6 +38,7 @@ pub struct AnalysisRow {
}
#[derive(FromRow)]
#[allow(dead_code)]
pub struct PassResultRow {
pub id: Uuid,
pub analysis_id: Uuid,

View File

@ -22,30 +22,20 @@
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;
use super::models::{
AnalysisRow, NewAnalysis, NewPassResult,
PassResultRow,
};
use super::models::{AnalysisRow, NewAnalysis, NewPassResult, PassResultRow};
pub async fn find_slug_by_sha256(
pool: &PgPool,
sha256: &str,
) -> Result<Option<String>, sqlx::Error> {
sqlx::query_scalar(
"SELECT slug FROM analyses WHERE sha256 = $1",
)
sqlx::query_scalar("SELECT slug FROM analyses WHERE sha256 = $1")
.bind(sha256)
.fetch_optional(pool)
.await
}
pub async fn find_by_slug(
pool: &PgPool,
slug: &str,
) -> Result<Option<AnalysisRow>, sqlx::Error> {
sqlx::query_as::<_, AnalysisRow>(
"SELECT * FROM analyses WHERE slug = $1",
)
pub async fn find_by_slug(pool: &PgPool, slug: &str) -> Result<Option<AnalysisRow>, sqlx::Error> {
sqlx::query_as::<_, AnalysisRow>("SELECT * FROM analyses WHERE slug = $1")
.bind(slug)
.fetch_optional(pool)
.await

View File

@ -16,9 +16,9 @@
// routes/upload.rs - returned from upload handler
// routes/analysis.rs - returned from analysis lookup
use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
#[derive(Serialize)]
@ -32,6 +32,7 @@ struct ErrorDetail {
message: String,
}
#[allow(dead_code)]
pub enum ApiError {
NoFile,
FileTooLarge { max_bytes: usize },
@ -71,37 +72,23 @@ impl IntoResponse for ApiError {
Self::NoFile => (
StatusCode::BAD_REQUEST,
"NO_FILE",
"No file was provided in the upload"
.to_string(),
"No file was provided in the upload".to_string(),
),
Self::FileTooLarge { max_bytes } => (
StatusCode::BAD_REQUEST,
"FILE_TOO_LARGE",
format!(
"File exceeds maximum allowed size of {} bytes",
max_bytes
),
),
Self::InvalidBinary { reason } => (
StatusCode::BAD_REQUEST,
"INVALID_BINARY",
reason,
),
Self::AnalysisFailed { reason } => (
StatusCode::INTERNAL_SERVER_ERROR,
"ANALYSIS_FAILED",
reason,
format!("File exceeds maximum allowed size of {} bytes", max_bytes),
),
Self::InvalidBinary { reason } => (StatusCode::BAD_REQUEST, "INVALID_BINARY", reason),
Self::AnalysisFailed { reason } => {
(StatusCode::INTERNAL_SERVER_ERROR, "ANALYSIS_FAILED", reason)
}
Self::NotFound { resource } => (
StatusCode::NOT_FOUND,
"NOT_FOUND",
format!("{resource} not found"),
),
Self::Internal { reason } => (
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL",
reason,
),
Self::Internal { reason } => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL", reason),
};
(

View File

@ -49,11 +49,7 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| {
EnvFilter::new(
"info,tower_http=debug",
)
}),
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=debug")),
)
.init();
@ -67,10 +63,8 @@ async fn main() -> anyhow::Result<()> {
.await
.context("failed to run database migrations")?;
let engine = axumortem_engine::AnalysisEngine::new()
.context(
"failed to initialize analysis engine",
)?;
let engine =
axumortem_engine::AnalysisEngine::new().context("failed to initialize analysis engine")?;
let config = Arc::new(config);
@ -83,16 +77,12 @@ async fn main() -> anyhow::Result<()> {
let layers = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(middleware::cors::layer(&config))
.layer(DefaultBodyLimit::max(
config.max_upload_size,
));
.layer(DefaultBodyLimit::max(config.max_upload_size));
let app =
routes::api_router().layer(layers).with_state(state);
let app = routes::api_router().layer(layers).with_state(state);
let bind_address = config.bind_address();
let listener =
tokio::net::TcpListener::bind(&bind_address)
let listener = tokio::net::TcpListener::bind(&bind_address)
.await
.context("failed to bind TCP listener")?;

View File

@ -13,17 +13,15 @@
// config.rs - AppConfig.cors_origin
// main.rs - applied as tower layer
use axum::http::header::{HeaderName, ACCEPT, CONTENT_TYPE};
use axum::http::Method;
use axum::http::header::{ACCEPT, CONTENT_TYPE, HeaderName};
use tower_http::cors::{Any, CorsLayer};
use crate::config::AppConfig;
const ALLOWED_METHODS: [Method; 3] =
[Method::GET, Method::POST, Method::OPTIONS];
const ALLOWED_METHODS: [Method; 3] = [Method::GET, Method::POST, Method::OPTIONS];
const ALLOWED_HEADERS: [HeaderName; 2] =
[CONTENT_TYPE, ACCEPT];
const ALLOWED_HEADERS: [HeaderName; 2] = [CONTENT_TYPE, ACCEPT];
pub fn layer(config: &AppConfig) -> CorsLayer {
let base = CorsLayer::new()

View File

@ -17,8 +17,8 @@
use std::collections::HashMap;
use axum::extract::{Path, State};
use axum::Json;
use axum::extract::{Path, State};
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;
@ -53,12 +53,9 @@ pub async fn get_by_slug(
resource: format!("analysis '{slug}'"),
})?;
let pass_rows =
queries::find_pass_results(&state.db, row.id)
.await?;
let pass_rows = queries::find_pass_results(&state.db, row.id).await?;
let passes: HashMap<String, serde_json::Value> =
pass_rows
let passes: HashMap<String, serde_json::Value> = pass_rows
.into_iter()
.map(|p| (p.pass_name, p.result))
.collect();

View File

@ -11,8 +11,8 @@
// Connects to:
// state.rs - AppState.db
use axum::extract::State;
use axum::Json;
use axum::extract::State;
use serde::Serialize;
use crate::state::AppState;
@ -23,12 +23,8 @@ pub(crate) struct HealthResponse {
database: &'static str,
}
pub async fn check(
State(state): State<AppState>,
) -> Json<HealthResponse> {
let db_status =
match sqlx::query("SELECT 1").execute(&state.db).await
{
pub async fn check(State(state): State<AppState>) -> Json<HealthResponse> {
let db_status = match sqlx::query("SELECT 1").execute(&state.db).await {
Ok(_) => "connected",
Err(_) => "disconnected",
};

View File

@ -18,8 +18,8 @@ mod analysis;
mod health;
mod upload;
use axum::routing::{get, post};
use axum::Router;
use axum::routing::{get, post};
use crate::state::AppState;
@ -27,8 +27,5 @@ pub fn api_router() -> Router<AppState> {
Router::new()
.route("/api/health", get(health::check))
.route("/api/upload", post(upload::handle))
.route(
"/api/analysis/{slug}",
get(analysis::get_by_slug),
)
.route("/api/analysis/{slug}", get(analysis::get_by_slug))
}

View File

@ -27,8 +27,8 @@
use std::collections::HashMap;
use std::sync::Arc;
use axum::extract::{Multipart, State};
use axum::Json;
use axum::extract::{Multipart, State};
use serde::Serialize;
use uuid::Uuid;
@ -42,9 +42,7 @@ use crate::state::AppState;
const SLUG_LENGTH: usize = 12;
const PASS_NAME_MAP: &[(&str, &str)] = &[
("disasm", "disassembly"),
];
const PASS_NAME_MAP: &[(&str, &str)] = &[("disasm", "disassembly")];
#[derive(Serialize)]
pub(crate) struct UploadResponse {
@ -56,32 +54,19 @@ pub async fn handle(
State(state): State<AppState>,
mut multipart: Multipart,
) -> Result<Json<UploadResponse>, ApiError> {
let (file_name, data) =
extract_file(&mut multipart).await?;
let (file_name, data) = extract_file(&mut multipart).await?;
let sha256 =
axumortem_engine::sha256_hex(&data);
let sha256 = axumortem_engine::sha256_hex(&data);
if let Some(slug) =
queries::find_slug_by_sha256(
&state.db, &sha256,
)
.await?
{
return Ok(Json(UploadResponse {
slug,
cached: true,
}));
if let Some(slug) = queries::find_slug_by_sha256(&state.db, &sha256).await? {
return Ok(Json(UploadResponse { slug, cached: true }));
}
let engine = Arc::clone(&state.engine);
let name_clone = file_name.clone();
let (ctx, report) =
tokio::task::spawn_blocking(move || {
engine.analyze(&data, &name_clone)
})
.await?;
tokio::task::spawn_blocking(move || engine.analyze(&data, &name_clone)).await?;
let fmt = ctx.format_result.as_ref();
let threat = ctx.threat_result.as_ref();
@ -91,29 +76,19 @@ pub async fn handle(
sha256,
file_name,
file_size: ctx.file_size as i64,
format: fmt
.map(|f| f.format.to_string())
.unwrap_or_default(),
architecture: fmt
.map(|f| f.architecture.to_string())
.unwrap_or_default(),
entry_point: fmt
.map(|f| f.entry_point as i64),
threat_score: threat
.map(|t| t.total_score as i32),
risk_level: threat
.map(|t| t.risk_level.to_string()),
format: fmt.map(|f| f.format.to_string()).unwrap_or_default(),
architecture: fmt.map(|f| f.architecture.to_string()).unwrap_or_default(),
entry_point: fmt.map(|f| f.entry_point as i64),
threat_score: threat.map(|t| t.total_score as i32),
risk_level: threat.map(|t| t.risk_level.to_string()),
slug: slug.clone(),
};
let mut tx = state.db.begin().await?;
let row =
queries::insert_analysis(&mut tx, &new_analysis)
.await?;
let row = queries::insert_analysis(&mut tx, &new_analysis).await?;
let pass_results =
build_pass_results(&ctx, &report, row.id)?;
let pass_results = build_pass_results(&ctx, &report, row.id)?;
for pr in &pass_results {
queries::insert_pass_result(&mut tx, pr).await?;
}
@ -126,9 +101,7 @@ pub async fn handle(
}))
}
async fn extract_file(
multipart: &mut Multipart,
) -> Result<(String, Vec<u8>), ApiError> {
async fn extract_file(multipart: &mut Multipart) -> Result<(String, Vec<u8>), ApiError> {
while let Some(field) = multipart
.next_field()
.await
@ -137,14 +110,8 @@ async fn extract_file(
})?
{
if field.name() == Some("file") {
let name = field
.file_name()
.unwrap_or("unknown")
.to_string();
let data = field
.bytes()
.await
.map_err(|e| ApiError::Internal {
let name = field.file_name().unwrap_or("unknown").to_string();
let data = field.bytes().await.map_err(|e| ApiError::Internal {
reason: e.to_string(),
})?;
return Ok((name, data.to_vec()));
@ -181,12 +148,9 @@ fn build_pass_results(
if let Some(ref r) = ctx.$field {
results.push(NewPassResult {
analysis_id,
pass_name: api_name($name)
.to_string(),
pass_name: api_name($name).to_string(),
result: serde_json::to_value(r)?,
duration_ms: durations
.get($name)
.map(|&d| d as i32),
duration_ms: durations.get($name).map(|&d| d as i32),
});
}
};

View File

@ -24,5 +24,6 @@ use crate::config::AppConfig;
pub struct AppState {
pub db: PgPool,
pub engine: Arc<AnalysisEngine>,
#[allow(dead_code)]
pub config: Arc<AppConfig>,
}

View File

@ -109,6 +109,7 @@ export function Component(): React.ReactElement {
width="100%"
height="100%"
preserveAspectRatio="none"
aria-hidden="true"
>
<filter id="grain-bat">
<feTurbulence
@ -248,7 +249,9 @@ export function Component(): React.ReactElement {
<footer className={styles.footer}>
<span>&copy; ANGELAMOS 2026</span>
<span className={styles.footerDesignation}>SYS AXM-BDE // UNIT-001</span>
<span className={styles.footerDesignation}>
SYS AXM-BDE {'//'} UNIT-001
</span>
<span>AXUMORTEM</span>
</footer>
</div>

View File

@ -0,0 +1,16 @@
# ©AngelaMos | 2026
# .ameba.yml
#
# Disabled rules document intentional patterns in this codebase
# - Naming/BlockParameterName: idiomatic Crystal uses short block params like |c|, |b|
# - Lint/NotNil: .not_nil! used as explicit runtime contract assertion after upstream
# validation; raising NilAssertionError is the desired crash mode for misconfiguration
# Raised thresholds match the natural shape of dispatch/subscriber handlers
Naming/BlockParameterName:
Enabled: false
Lint/NotNil:
Enabled: false
Metrics/CyclomaticComplexity:
MaxComplexity: 20

View File

@ -1,5 +1,9 @@
version: 2.0
shards:
ameba:
git: https://github.com/crystal-ameba/ameba.git
version: 1.6.4
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.11.0

View File

@ -33,3 +33,6 @@ development_dependencies:
webmock:
github: manastech/webmock.cr
version: ~> 0.9
ameba:
github: crystal-ameba/ameba
version: ~> 1.6.4

View File

@ -79,7 +79,7 @@ module CRE::Aws
end
private def canonical_query(query : String?) : String
return "" unless query && !query.empty?
return "" if query.nil? || query.empty?
params = [] of {String, String}
query.split('&') do |pair|
eq = pair.index('=')
@ -99,7 +99,7 @@ module CRE::Aws
private def canonical_headers_and_list(headers : HTTP::Headers) : {String, String}
sorted = headers.to_a.map { |name, values|
{name.downcase, values.first.strip.gsub(/\s+/, " ")}
}.sort_by { |entry| entry[0] }
}.sort_by! { |entry| entry[0] }
canonical = sorted.map { |k, v| "#{k}:#{v}\n" }.join
list = sorted.map(&.[0]).join(';')

View File

@ -32,7 +32,7 @@ module CRE::Cli::Commands
CRE::Policy::Evaluator.new(bus, persist).evaluate_all
sleep 0.1.seconds
violations = drain(ch).select(&.is_a?(CRE::Events::PolicyViolation)).map(&.as(CRE::Events::PolicyViolation))
violations = drain(ch).select(CRE::Events::PolicyViolation).map(&.as(CRE::Events::PolicyViolation))
bus.stop
persist.close

View File

@ -19,7 +19,7 @@ module CRE::Demo
bus.run
tui.start
script_fiber = spawn do
spawn do
run_script(bus, seconds)
end

View File

@ -59,7 +59,7 @@ module CRE::Engine
@persistence.rotations.update_state(rotation_id, Persistence::RotationState::Applying)
@bus.publish Events::RotationStepCompleted.new(c.id, rotation_id, :generate)
current_step = :apply
current_step = :apply # ameba:disable Lint/UselessAssign — read in rescue
@bus.publish Events::RotationStepStarted.new(c.id, rotation_id, :apply)
rotator.apply(c, new_secret)
@persistence.rotations.update_state(rotation_id, Persistence::RotationState::Verifying)
@ -83,8 +83,8 @@ module CRE::Engine
if (ns = new_secret) && (current_step == :apply || current_step == :verify)
begin
rotator.rollback_apply(c, ns)
rescue rb
Log.error(exception: rb) { "rollback_apply failed for credential #{c.id}" }
rescue error
Log.error(exception: error) { "rollback_apply failed for credential #{c.id}" }
end
end
finalize_failure(c, rotation_id, current_step, ex)

View File

@ -40,11 +40,11 @@ module CRE::Persistence::Postgres
@audit ||= AuditRepo.new(@db)
end
def transaction(&block : ->) : Nil
def transaction(& : ->) : Nil
@db.transaction { yield }
end
def with_advisory_lock(key : Int64, &block : ->) : Nil
def with_advisory_lock(key : Int64, & : ->) : Nil
@db.transaction do |tx|
tx.connection.exec("SELECT pg_advisory_xact_lock($1)", key)
yield

View File

@ -48,11 +48,11 @@ module CRE::Persistence::Sqlite
@audit ||= AuditRepo.new(@db)
end
def transaction(&block : ->) : Nil
def transaction(& : ->) : Nil
@db.transaction { yield }
end
def with_advisory_lock(key : Int64, &block : ->) : Nil
def with_advisory_lock(key : Int64, & : ->) : Nil
_ = key # SQLite is single-process; the mutex is sufficient
@mutex.synchronize { yield }
end

View File

@ -25,7 +25,7 @@ module CRE::Policy::Dsl
# `enforce :rotate_immediatly` fail at compile time. Splat-symbol args
# (`notify_via :telegram, :structured_log`) are validated at policy
# registration time and raise `BuilderError` on typos.
def policy(name : String, &block)
def policy(name : String, &)
builder = CRE::Policy::Builder.new(name)
with builder yield
CRE::Policy::REGISTRY << builder.build

View File

@ -54,7 +54,7 @@ module CRE::Policy
return if @policies.empty?
seen = Set(UUID).new
@policies.group_by(&.max_age).each do |max_age, policies_with_age|
@policies.group_by(&.max_age).each do |max_age, _|
@persistence.credentials.overdue(now, max_age).each do |c|
next if seen.includes?(c.id)
seen << c.id

View File

@ -50,7 +50,7 @@ module CRE::Rotators
pending = pending_path(path)
existing = File.exists?(path) ? File.read(path) : ""
lines = existing.lines(chomp: true).reject { |line| line.strip.starts_with?("#{key}=") }
lines = existing.lines(chomp: true).reject(&.strip.starts_with?("#{key}="))
new_value = String.new(s.ciphertext)
lines << "#{key}=#{new_value}"

View File

@ -1,31 +1,39 @@
# ©AngelaMos | 2026
# .golangci.yml
version: "2"
run:
timeout: 5m
linters:
default: none
enable:
- errcheck
- govet
- staticcheck
- unused
- gosimple
- ineffassign
- typecheck
- gofmt
- gocritic
- gosec
- misspell
- unconvert
- unparam
- prealloc
linters-settings:
settings:
errcheck:
exclude-functions:
- fmt.Fprint
- fmt.Fprintf
- fmt.Fprintln
- (io.Closer).Close
gocritic:
enabled-tags:
- diagnostic
- performance
disabled-checks:
- hugeParam
- rangeValCopy
gosec:
excludes:
- G304
- G704

View File

@ -73,7 +73,7 @@ func runGenerate(_ *cobra.Command, args []string) error {
}
if outputFile != "" {
if err := os.WriteFile(outputFile, data, 0o644); err != nil {
if err := os.WriteFile(outputFile, data, 0o600); err != nil {
return fmt.Errorf("write output: %w", err)
}
fmt.Fprintf(os.Stderr, " %s SBOM written to %s (%s)\n",

View File

@ -75,7 +75,7 @@ func runVuln(cmd *cobra.Command, args []string) error {
}
func queryVulns(ctx context.Context, result *types.ScanResult) (*types.VulnReport, error) {
var allPkgs []types.Package
allPkgs := make([]types.Package, 0, len(result.Graphs))
for _, g := range result.Graphs {
allPkgs = append(allPkgs, graph.AllPackages(g)...)
}
@ -85,7 +85,7 @@ func queryVulns(ctx context.Context, result *types.ScanResult) (*types.VulnRepor
c, err := vuln.NewCache(vuln.DefaultCachePath(), 24*time.Hour)
if err == nil {
cache = c
defer cache.Close()
defer func() { _ = cache.Close() }()
}
}

View File

@ -38,7 +38,7 @@ func (p *GoModParser) Parse(dir string) (*types.DependencyGraph, error) {
if err != nil {
return nil, fmt.Errorf("open go.mod: %w", err)
}
defer modFile.Close()
defer func() { _ = modFile.Close() }()
var moduleName string
directDeps := make(map[string]string)
@ -120,13 +120,13 @@ func (p *GoModParser) Parse(dir string) (*types.DependencyGraph, error) {
graph.AddPackage(pkg)
}
parseGoModGraph(dir, moduleName, graph)
parseGoModGraph(dir, graph)
computeDepthLevels(graph)
return graph, nil
}
func parseGoModGraph(dir, moduleName string, graph *types.DependencyGraph) {
func parseGoModGraph(dir string, graph *types.DependencyGraph) {
cmd := exec.Command("go", "mod", "graph")
cmd.Dir = dir
out, err := cmd.Output()
@ -140,8 +140,8 @@ func parseGoModGraph(dir, moduleName string, graph *types.DependencyGraph) {
if len(parts) != 2 {
continue
}
parentPURL := goDepToPURL(parts[0], moduleName)
childPURL := goDepToPURL(parts[1], moduleName)
parentPURL := goDepToPURL(parts[0])
childPURL := goDepToPURL(parts[1])
if _, exists := graph.Nodes[childPURL]; !exists {
continue
@ -154,7 +154,7 @@ func parseGoModGraph(dir, moduleName string, graph *types.DependencyGraph) {
}
}
func goDepToPURL(dep, moduleName string) string {
func goDepToPURL(dep string) string {
if !strings.Contains(dep, "@") {
return fmt.Sprintf("pkg:golang/%s", dep)
}
@ -195,7 +195,7 @@ func parseGoSum(path string) map[string][]types.Checksum {
if err != nil {
return checksums
}
defer f.Close()
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
for scanner.Scan() {

View File

@ -23,7 +23,7 @@ type Cache struct {
func NewCache(dbPath string, ttl time.Duration) (*Cache, error) {
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0o755); err != nil {
if err := os.MkdirAll(dir, 0o750); err != nil {
return nil, fmt.Errorf("create cache dir: %w", err)
}
@ -40,7 +40,7 @@ func NewCache(dbPath string, ttl time.Duration) (*Cache, error) {
PRIMARY KEY (purl, source)
)`
if _, err := db.Exec(createSQL); err != nil {
db.Close()
_ = db.Close()
return nil, fmt.Errorf("create cache table: %w", err)
}

Some files were not shown because too many files have changed in this diff Show More