Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
452354a8e3 | ||
|
|
5fb37bfbaf | ||
|
|
cdd8c292db | ||
|
|
9cdad90e3a | ||
|
|
da1a21ee47 | ||
|
|
1fdc13aa73 | ||
|
|
374b320ba4 | ||
|
|
4dbb39b8ee | ||
|
|
26de69b6e2 | ||
|
|
f3d5a8161d | ||
|
|
ffaae2b459 | ||
|
|
8b8dec0754 | ||
|
|
cd6d8b5119 | ||
|
|
a303a0d7f5 | ||
|
|
f711e6babb | ||
|
|
00fb35096d | ||
|
|
d33ce4d64c | ||
|
|
61ad96bc1a | ||
|
|
98505ed7b4 | ||
|
|
a961501b6d | ||
|
|
0a54751b7e | ||
|
|
313237f167 | ||
|
|
de1a5c2893 | ||
|
|
e93368fa84 | ||
|
|
067a257b6a | ||
|
|
07b754090a | ||
|
|
5d3b3e96bb | ||
|
|
3742fc63fe | ||
|
|
48121aec0d | ||
|
|
2618fb9e45 | ||
|
|
a747e4c1bb | ||
|
|
51eed69fe4 | ||
|
|
8caedd11d5 | ||
|
|
3b5fe0c116 | ||
|
|
a2575233b7 | ||
|
|
8236789781 | ||
|
|
02967629ff | ||
|
|
ff6d7155cd |
@@ -0,0 +1,55 @@
|
|||||||
|
name: rust-binary-build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
paths:
|
||||||
|
- 'rust-toolchain.toml'
|
||||||
|
- 'adk-rust/**'
|
||||||
|
- 'scripts/package_rust_release_binaries.py'
|
||||||
|
- '.github/workflows/rust-binary-build.yml'
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux-x86_64:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pinned Rust toolchain
|
||||||
|
run: |
|
||||||
|
rustup toolchain install 1.94.0 --profile minimal --component rustfmt --component clippy
|
||||||
|
rustup override set 1.94.0
|
||||||
|
rustup show active-toolchain
|
||||||
|
cargo +1.94.0 --version
|
||||||
|
rustc +1.94.0 --version
|
||||||
|
|
||||||
|
- name: Build release binaries
|
||||||
|
run: cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release
|
||||||
|
|
||||||
|
- name: Package release binaries
|
||||||
|
run: |
|
||||||
|
python3 scripts/package_rust_release_binaries.py \
|
||||||
|
--release-dir adk-rust/target/release \
|
||||||
|
--out-dir dist/awatch-rus-linux-x86_64 \
|
||||||
|
--archive dist/awatch-rus-linux-x86_64-release-binaries.tar.gz \
|
||||||
|
--target linux-x86_64
|
||||||
|
|
||||||
|
- name: Upload release binaries artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: awatch-rus-linux_x86_64-release-binaries
|
||||||
|
path: |
|
||||||
|
dist/awatch-rus-linux_x86_64-release-binaries.tar.gz
|
||||||
|
dist/awatch-rus-linux_x86_64-release-binaries.tar.gz.sha256
|
||||||
|
dist/awatch-rus-linux_x86_64/BINARIES.txt
|
||||||
|
dist/awatch-rus-linux_x86_64/SHA256SUMS.txt
|
||||||
|
dist/awatch-rus-linux_x86_64/BUILD_MANIFEST.json
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 30
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: Rust clippy diagnostic
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- codex/rust-professionalization
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detmir-portal-clippy-diagnostic:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust 1.85 with rustfmt and clippy
|
||||||
|
run: |
|
||||||
|
rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy
|
||||||
|
rustup default 1.85.0
|
||||||
|
|
||||||
|
- name: Capture detmir-portal clippy output
|
||||||
|
working-directory: adk-rust
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
cargo clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1
|
||||||
|
status=$?
|
||||||
|
echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt
|
||||||
|
tail -n 240 ../detmir-portal-clippy.log
|
||||||
|
exit ${status}
|
||||||
|
|
||||||
|
- name: Upload detmir-portal clippy log
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: detmir-portal-clippy-log
|
||||||
|
path: |
|
||||||
|
detmir-portal-clippy.log
|
||||||
|
detmir-portal-clippy-status.txt
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Rust professionalization check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'rust-toolchain.toml'
|
||||||
|
- 'adk-rust/crates/detmir-core/**'
|
||||||
|
- 'adk-rust/crates/detmir-portal/**'
|
||||||
|
- 'scripts/check_private_config_guard.sh'
|
||||||
|
- 'scripts/check_portal_contract_sync.mjs'
|
||||||
|
- '.github/workflows/rust-professionalization-check.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rust-check:
|
||||||
|
name: changed Rust crates smoke
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 40
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pinned Rust toolchain
|
||||||
|
run: |
|
||||||
|
rustup toolchain install 1.94.0 --profile minimal --component rustfmt --component clippy
|
||||||
|
rustup override set 1.94.0
|
||||||
|
rustup show active-toolchain
|
||||||
|
cargo +1.94.0 --version
|
||||||
|
rustc +1.94.0 --version
|
||||||
|
|
||||||
|
- name: Cargo fmt check
|
||||||
|
working-directory: adk-rust
|
||||||
|
run: cargo +1.94.0 fmt --all -- --check
|
||||||
|
|
||||||
|
- name: Test detmir-core
|
||||||
|
working-directory: adk-rust
|
||||||
|
run: cargo +1.94.0 test -p detmir-core
|
||||||
|
|
||||||
|
- name: Test detmir-portal
|
||||||
|
working-directory: adk-rust
|
||||||
|
run: cargo +1.94.0 test -p detmir-portal
|
||||||
|
|
||||||
|
- name: Clippy detmir-core
|
||||||
|
working-directory: adk-rust
|
||||||
|
run: cargo +1.94.0 clippy -p detmir-core --all-targets -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy detmir-portal with captured log
|
||||||
|
working-directory: adk-rust
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
cargo +1.94.0 clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1
|
||||||
|
status=$?
|
||||||
|
echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt
|
||||||
|
tail -n 80 ../detmir-portal-clippy.log
|
||||||
|
exit ${status}
|
||||||
|
|
||||||
|
- name: Upload detmir-portal clippy log
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: detmir-portal-clippy-log
|
||||||
|
path: |
|
||||||
|
detmir-portal-clippy.log
|
||||||
|
detmir-portal-clippy-status.txt
|
||||||
|
|
||||||
|
- name: Private config guard
|
||||||
|
run: bash scripts/check_private_config_guard.sh
|
||||||
|
|
||||||
|
- name: Portal contract sync
|
||||||
|
run: node scripts/check_portal_contract_sync.mjs
|
||||||
@@ -5,6 +5,7 @@ on:
|
|||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rust-workspace:
|
rust-workspace:
|
||||||
@@ -13,19 +14,22 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust 1.85
|
- name: Install pinned Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@1.85.0
|
run: |
|
||||||
with:
|
rustup toolchain install 1.94.0 --profile minimal --component rustfmt --component clippy
|
||||||
components: rustfmt, clippy
|
rustup override set 1.94.0
|
||||||
|
rustup show active-toolchain
|
||||||
|
cargo +1.94.0 --version
|
||||||
|
rustc +1.94.0 --version
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: cargo fmt --manifest-path adk-rust/Cargo.toml --all -- --check
|
run: cargo +1.94.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --manifest-path adk-rust/Cargo.toml --workspace
|
run: cargo +1.94.0 test --manifest-path adk-rust/Cargo.toml --workspace
|
||||||
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings
|
run: cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
- name: Release build
|
- name: Release build
|
||||||
run: cargo build --manifest-path adk-rust/Cargo.toml --workspace --release
|
run: cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release
|
||||||
|
|||||||
@@ -5,21 +5,17 @@ AWatch-rus - программный комплекс операционного
|
|||||||
корпоративной ИТ-инфраструктуры на базе ActivityWatch, Rust-сервисов
|
корпоративной ИТ-инфраструктуры на базе ActivityWatch, Rust-сервисов
|
||||||
автоматизации, Grafana/Prometheus-витрин и модулей расследования инцидентов.
|
автоматизации, Grafana/Prometheus-витрин и модулей расследования инцидентов.
|
||||||
|
|
||||||
Проект не позиционируется как сертифицированная DLP/SIEM/EDR/XDR/СЗИ. DLP,
|
Проект не позиционируется как сертифицированная DLP/SIEM/EDR/XDR/СЗИ,хотя DLP,evidence и Hayabusa используются в проекте.
|
||||||
evidence и Hayabusa используются как прикладные модули внутри платформы
|
|
||||||
операционного контроля и технического аудита.
|
|
||||||
|
|
||||||
## Назначение
|
## Назначение
|
||||||
|
|
||||||
- AWatch-rus Workforce: активность сотрудников, загрузка, RDP/1C/рабочие
|
- AWatch-rus Workforce: активность сотрудников, загрузка, RDP/1C/рабочие
|
||||||
приложения и управленческие отчеты для владельца бизнеса.
|
приложения и управленческие отчеты для владельца бизнеса.
|
||||||
- AWatch-rus Security: DLP-сигналы, evidence, очередь кейсов и audit действий
|
- AWatch-rus Security: DLP-сигналы, evidence, очередь кейсов и audit действий оператора без заявления продукта как сертифицированной СЗИ.
|
||||||
оператора без заявления продукта как сертифицированной СЗИ.
|
- AWatch-rus Forensics: цепочки событий, Hayabusa/offline-разбор и материалы для внутреннего расследования.
|
||||||
- AWatch-rus Forensics: цепочки событий, Hayabusa/offline-разбор и материалы для
|
|
||||||
внутреннего расследования.
|
|
||||||
- Контроль доступности и свежести данных ActivityWatch.
|
- Контроль доступности и свежести данных ActivityWatch.
|
||||||
- Учет активного времени, RDP-сессий, окон, приложений и рабочих интервалов.
|
- Учет активного времени, Windows RDP-сессий окон, приложений и рабочих интервалов а также активности пользователей в Linux/Unix системах.
|
||||||
- Витрины Grafana для администратора, оператора ИБ и руководителя.
|
- витрины Grafana для администратора, оператора ИБ и руководителя(dashboards).
|
||||||
- Автоматизация runbook-проверок, health-check, SLO и безопасного auto-heal.
|
- Автоматизация runbook-проверок, health-check, SLO и безопасного auto-heal.
|
||||||
- Сбор evidence по инцидентам и аудит действий оператора.
|
- Сбор evidence по инцидентам и аудит действий оператора.
|
||||||
|
|
||||||
@@ -28,18 +24,17 @@ evidence и Hayabusa используются как прикладные мод
|
|||||||
Основной серверный runtime AWatch-rus переведен на Rust: status/check/auto-heal,
|
Основной серверный runtime AWatch-rus переведен на Rust: status/check/auto-heal,
|
||||||
SLO, worktime, DLP server-side helpers, evidence и install-kit tooling.
|
SLO, worktime, DLP server-side helpers, evidence и install-kit tooling.
|
||||||
|
|
||||||
Python в репозитории остается для вспомогательных направлений: Telegram bot
|
Python, присутствующий в коде репозитория, остается для вспомогательных направлений: Telegram bot
|
||||||
runtime, OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти
|
runtime(для оперативного оповещения), OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти части не являются ядром Rust-first runtime.
|
||||||
части не являются ядром Rust-first runtime.
|
|
||||||
|
|
||||||
Портальный слой зафиксирован как Rust server-rendered HTML + HTMX-compatible
|
Портальный слой зафиксирован как Rust server-rendered HTML + HTMX-compatible
|
||||||
JSON API, OpenAPI и TypeScript declarations. Dioxus не используется и не
|
JSON API, OpenAPI и TypeScript declarations. Dioxus не используется и не
|
||||||
рассматривается для Pilot v1.0. React, Tauri и Electron также не входят в
|
рассматривается для Pilot v1.0. React, Tauri и Electron также не входят в
|
||||||
текущий основной UI.
|
текущий основной UI, но возможна их интеграция в проект.
|
||||||
|
|
||||||
## Product Evolution
|
## Product Evolution
|
||||||
|
|
||||||
AWatch-rus уже является рабочей платформой Workforce + Security + Forensics.
|
AWatch-rus является рабочей платформой Workforce + Security + Forensics.
|
||||||
Архитектура предусматривает расширение на агентные и agentless-источники
|
Архитектура предусматривает расширение на агентные и agentless-источники
|
||||||
данных. Planned/Future элементы ниже не являются реализованной функциональностью
|
данных. Planned/Future элементы ниже не являются реализованной функциональностью
|
||||||
и не должны трактоваться как готовые collectors или integrations.
|
и не должны трактоваться как готовые collectors или integrations.
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
|
||||||
|
//! Shared production primitives for AWatch-rus.
|
||||||
|
//!
|
||||||
|
//! This crate intentionally stays small and dependency-light. It contains the
|
||||||
|
//! status, exit-code and runtime-configuration guardrails that are reused by
|
||||||
|
//! operational binaries and health/check tooling. Keep business-specific portal,
|
||||||
|
//! DLP or workforce logic out of this crate.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chrono::{DateTime, SecondsFormat, Utc};
|
use chrono::{DateTime, SecondsFormat, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Normalized health/check status used by CLI tools, probes and JSON payloads.
|
||||||
|
///
|
||||||
|
/// CONTRACT: serialized values are uppercase and must remain stable because
|
||||||
|
/// deployment scripts, smoke checks and dashboards can key off these strings.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
pub enum StatusLevel {
|
pub enum StatusLevel {
|
||||||
|
/// Component is healthy and the check passed.
|
||||||
Ok,
|
Ok,
|
||||||
|
/// Component works, but a risk or degraded condition needs attention.
|
||||||
Warn,
|
Warn,
|
||||||
|
/// Component check failed or a required dependency is unavailable.
|
||||||
Fail,
|
Fail,
|
||||||
|
/// Component did not provide enough information for a reliable status.
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusLevel {
|
impl StatusLevel {
|
||||||
|
/// Return the stable uppercase representation used in human and JSON output.
|
||||||
pub fn as_str(self) -> &'static str {
|
pub fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Ok => "OK",
|
Self::Ok => "OK",
|
||||||
@@ -23,6 +41,10 @@ impl StatusLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map status to the process exit code expected by operational checks.
|
||||||
|
///
|
||||||
|
/// CONTRACT: `WARN` exits as a failed check rather than success so that
|
||||||
|
/// automation does not silently ignore degraded production state.
|
||||||
pub fn exit_code(self) -> i32 {
|
pub fn exit_code(self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::Ok => exit_codes::OK,
|
Self::Ok => exit_codes::OK,
|
||||||
@@ -48,23 +70,39 @@ impl From<&str> for StatusLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stable process exit codes for AWatch-rus operational binaries.
|
||||||
|
///
|
||||||
|
/// CONTRACT: keep these numeric values stable. Shell scripts, systemd units,
|
||||||
|
/// smoke tests and runbooks can depend on them.
|
||||||
pub mod exit_codes {
|
pub mod exit_codes {
|
||||||
|
/// Successful execution.
|
||||||
pub const OK: i32 = 0;
|
pub const OK: i32 = 0;
|
||||||
|
/// Unexpected runtime or IO error.
|
||||||
pub const ERROR: i32 = 1;
|
pub const ERROR: i32 = 1;
|
||||||
|
/// Health/check policy failed or returned a degraded status.
|
||||||
pub const CHECK_FAILED: i32 = 2;
|
pub const CHECK_FAILED: i32 = 2;
|
||||||
|
/// A safety policy denied a requested action.
|
||||||
pub const POLICY_DENIED: i32 = 3;
|
pub const POLICY_DENIED: i32 = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the current UTC timestamp in compact RFC3339/Zulu format.
|
||||||
pub fn now_utc_rfc3339() -> String {
|
pub fn now_utc_rfc3339() -> String {
|
||||||
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
|
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse an RFC3339 timestamp and normalize it to UTC.
|
||||||
pub fn parse_utc_rfc3339(value: &str) -> Result<DateTime<Utc>> {
|
pub fn parse_utc_rfc3339(value: &str) -> Result<DateTime<Utc>> {
|
||||||
DateTime::parse_from_rfc3339(value)
|
DateTime::parse_from_rfc3339(value)
|
||||||
.with_context(|| format!("invalid RFC3339 timestamp: {value}"))
|
.with_context(|| format!("invalid RFC3339 timestamp: {value}"))
|
||||||
.map(|ts| ts.with_timezone(&Utc))
|
.map(|ts| ts.with_timezone(&Utc))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runtime configuration guardrails.
|
||||||
|
///
|
||||||
|
/// SECURITY: these helpers are deliberately conservative. They reject empty,
|
||||||
|
/// documentation, TEST-NET and common placeholder values before a component is
|
||||||
|
/// allowed to run in production mode. This prevents demo-safe examples from
|
||||||
|
/// accidentally becoming live runtime configuration.
|
||||||
pub mod runtime_guard {
|
pub mod runtime_guard {
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
@@ -82,6 +120,11 @@ pub mod runtime_guard {
|
|||||||
"PASSWORD",
|
"PASSWORD",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Return true when a value looks like a public/demo placeholder.
|
||||||
|
///
|
||||||
|
/// RATIONALE: AWatch-rus documentation intentionally uses TEST-NET ranges
|
||||||
|
/// and HOST-EXAMPLE markers. Production binaries should fail closed when
|
||||||
|
/// such values reach runtime configuration.
|
||||||
pub fn is_runtime_placeholder(value: &str) -> bool {
|
pub fn is_runtime_placeholder(value: &str) -> bool {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -106,6 +149,7 @@ pub mod runtime_guard {
|
|||||||
|| (normalized.starts_with('<') && normalized.ends_with('>'))
|
|| (normalized.starts_with('<') && normalized.ends_with('>'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return true when a value is unsafe for a secret-like configuration field.
|
||||||
pub fn is_secret_placeholder(value: &str) -> bool {
|
pub fn is_secret_placeholder(value: &str) -> bool {
|
||||||
is_runtime_placeholder(value)
|
is_runtime_placeholder(value)
|
||||||
|| matches!(
|
|| matches!(
|
||||||
@@ -114,6 +158,10 @@ pub mod runtime_guard {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure a required runtime value is not empty or demo-only.
|
||||||
|
///
|
||||||
|
/// SECURITY: callers should invoke this before opening network connections,
|
||||||
|
/// starting ingestion or enabling exporters in production mode.
|
||||||
pub fn ensure_runtime_value(name: &str, value: &str, context: &str) -> Result<()> {
|
pub fn ensure_runtime_value(name: &str, value: &str, context: &str) -> Result<()> {
|
||||||
if is_runtime_placeholder(value) {
|
if is_runtime_placeholder(value) {
|
||||||
bail!("{name} contains an empty/example/TEST-NET value while {context}");
|
bail!("{name} contains an empty/example/TEST-NET value while {context}");
|
||||||
@@ -121,6 +169,7 @@ pub mod runtime_guard {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure a required secret is not empty or an obvious placeholder.
|
||||||
pub fn ensure_secret_value(name: &str, value: &str, context: &str) -> Result<()> {
|
pub fn ensure_secret_value(name: &str, value: &str, context: &str) -> Result<()> {
|
||||||
if is_secret_placeholder(value) {
|
if is_secret_placeholder(value) {
|
||||||
bail!("{name} contains an empty/example secret value while {context}");
|
bail!("{name} contains an empty/example secret value while {context}");
|
||||||
@@ -128,6 +177,7 @@ pub mod runtime_guard {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure an iterator of runtime values is non-empty and production-safe.
|
||||||
pub fn ensure_runtime_values<'a>(
|
pub fn ensure_runtime_values<'a>(
|
||||||
name: &str,
|
name: &str,
|
||||||
values: impl IntoIterator<Item = &'a String>,
|
values: impl IntoIterator<Item = &'a String>,
|
||||||
@@ -144,6 +194,12 @@ pub mod runtime_guard {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate a complete InfluxDB exporter configuration block.
|
||||||
|
///
|
||||||
|
/// CONTRACT: when an exporter is enabled, URL, org, bucket, token and host
|
||||||
|
/// list must all be real runtime values. A partial/demo exporter config is
|
||||||
|
/// more dangerous than a disabled exporter because it creates false
|
||||||
|
/// confidence in monitoring readiness.
|
||||||
pub fn ensure_influx_runtime_config(
|
pub fn ensure_influx_runtime_config(
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
url: &str,
|
url: &str,
|
||||||
|
|||||||
@@ -21,3 +21,7 @@ tiny_http.workspace = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
comparison_chain = "allow"
|
||||||
|
search_is_some = "allow"
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
//! Liveness probe payload.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: `/healthz` is intentionally shallow. It proves that the portal
|
||||||
|
//! process can answer HTTP, while dependency checks belong to `/readyz`.
|
||||||
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::now;
|
use crate::now;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
//! Configuration and request-bound validation for production portal routes.
|
||||||
|
//!
|
||||||
|
//! RATIONALE: the portal can aggregate reports, evidence and external service
|
||||||
|
//! payloads. Query and body limits keep pilot installations responsive and make
|
||||||
|
//! expensive report routes fail closed instead of exhausting memory or blocking
|
||||||
|
//! the single-process runtime.
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
@@ -30,6 +37,10 @@ pub(crate) fn validate_portal_config(args: &Cli) -> Result<()> {
|
|||||||
if port == 0 {
|
if port == 0 {
|
||||||
return Err(anyhow!("invalid config port: expected 1..65535"));
|
return Err(anyhow!("invalid config port: expected 1..65535"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RATIONALE: page and date limits protect heavy report endpoints while
|
||||||
|
// preserving monthly pilot reporting. Hard upper bounds prevent accidental
|
||||||
|
// production overrides from turning the portal into an unbounded exporter.
|
||||||
if args.max_page_size == 0 || args.max_page_size > MAX_ALLOWED_PAGE_SIZE {
|
if args.max_page_size == 0 || args.max_page_size > MAX_ALLOWED_PAGE_SIZE {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"invalid config max_page_size: expected 1..={MAX_ALLOWED_PAGE_SIZE}"
|
"invalid config max_page_size: expected 1..={MAX_ALLOWED_PAGE_SIZE}"
|
||||||
@@ -66,6 +77,10 @@ pub(crate) fn validate_portal_config(args: &Cli) -> Result<()> {
|
|||||||
"invalid config max_request_body_bytes: expected 1024..={MAX_ALLOWED_REQUEST_BODY_BYTES}"
|
"invalid config max_request_body_bytes: expected 1024..={MAX_ALLOWED_REQUEST_BODY_BYTES}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SECURITY: environment and module names can reach metrics/log labels.
|
||||||
|
// Restrict them to short ASCII tokens to avoid label injection and runaway
|
||||||
|
// cardinality from free-form deployment names.
|
||||||
if !is_safe_environment_name(&args.environment) {
|
if !is_safe_environment_name(&args.environment) {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"invalid config environment: use 1..32 chars from A-Z, a-z, 0-9, _, -"
|
"invalid config environment: use 1..32 chars from A-Z, a-z, 0-9, _, -"
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
//! Structured HTTP access logging for the portal runtime.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: logs are emitted as single-line JSON to stderr so systemd/journald,
|
||||||
|
//! container runtimes and log forwarders can parse them without scraping free
|
||||||
|
//! text. Do not log raw request bodies, secrets, evidence bytes or personal
|
||||||
|
//! payloads here.
|
||||||
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use tiny_http::StatusCode;
|
use tiny_http::StatusCode;
|
||||||
|
|
||||||
@@ -21,6 +28,10 @@ pub(crate) fn log_http_request(
|
|||||||
} else {
|
} else {
|
||||||
Value::Null
|
Value::Null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SECURITY: include routing/correlation fields, but do not include query
|
||||||
|
// values, request body, headers or tokens. Those can contain employee data,
|
||||||
|
// screenshots, evidence references or API keys.
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
json!({
|
json!({
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
//! In-process Prometheus-style metrics for the portal.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: metric names and label keys are part of the operational contract
|
||||||
|
//! used by dashboards and smoke checks. Additive metrics are allowed; renaming
|
||||||
|
//! existing metrics requires synchronized dashboard/documentation changes.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt::Write as FmtWrite;
|
use std::fmt::Write as FmtWrite;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
@@ -179,5 +185,7 @@ pub(crate) fn render_prometheus_metrics(args: &Cli) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prom_escape(value: &str) -> String {
|
fn prom_escape(value: &str) -> String {
|
||||||
|
// SECURITY: metric label values are route/module tokens, but escaping keeps
|
||||||
|
// the endpoint safe if future callers pass proxy-derived values.
|
||||||
value.replace('\\', "\\\\").replace('"', "\\\"")
|
value.replace('\\', "\\\\").replace('"', "\\\"")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
//! Production-facing portal runtime support.
|
||||||
|
//!
|
||||||
|
//! This module groups the cross-cutting concerns that must stay consistent
|
||||||
|
//! across all portal routes: health/readiness/version contracts, query and
|
||||||
|
//! configuration limits, structured logging, Prometheus-style metrics and
|
||||||
|
//! request correlation metadata.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: keep this module free from role-specific business rendering. It is
|
||||||
|
//! the operational boundary around the portal, not the workforce/security report
|
||||||
|
//! implementation itself.
|
||||||
|
|
||||||
pub(crate) mod health;
|
pub(crate) mod health;
|
||||||
pub(crate) mod limits;
|
pub(crate) mod limits;
|
||||||
pub(crate) mod logging;
|
pub(crate) mod logging;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
//! Readiness probe payload.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: `/readyz` checks whether the portal is safe to receive normal
|
||||||
|
//! traffic. It must remain conservative: configuration errors and broken state
|
||||||
|
//! storage make the process `not_ready`; optional integrations can report
|
||||||
|
//! `disabled`, `not_required` or `contract_only` without failing the whole probe.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
//! Request correlation and route classification for portal observability.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: generated route names must not expose volatile identifiers such as
|
||||||
|
//! case IDs, candidate IDs or evidence IDs; use route templates instead.
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||||
@@ -73,7 +78,7 @@ fn request_header(request: &Request, name: &str) -> Option<String> {
|
|||||||
request
|
request
|
||||||
.headers()
|
.headers()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|header| header.field.to_string().eq_ignore_ascii_case(name))
|
.find(|header| header.field.as_str().as_str().eq_ignore_ascii_case(name))
|
||||||
.map(|header| header.value.as_str().to_string())
|
.map(|header| header.value.as_str().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
//! Build/version probe payload.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: `/version` is used by smoke tests, runbooks and release evidence.
|
||||||
|
//! Keep field names stable and add new fields only in a backward-compatible way.
|
||||||
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::{Cli, PORTAL_SCHEMA_VERSION};
|
use crate::{Cli, PORTAL_SCHEMA_VERSION};
|
||||||
|
|||||||
@@ -13,23 +13,22 @@ Forensics с прозрачными rule-based объяснениями.
|
|||||||
| Продукт | Публичная категория | Сильная сторона | Как позиционировать AWatch-rus рядом |
|
| Продукт | Публичная категория | Сильная сторона | Как позиционировать AWatch-rus рядом |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| ActivityWatch | Open-source automated time tracker | Локальный, открытый и понятный сбор активности приложений и сайтов | AWatch-rus развивает этот подход в пилотный корпоративный контур с ролями, отчетами, Risk Narrative и эксплуатационной документацией |
|
| ActivityWatch | Open-source automated time tracker | Локальный, открытый и понятный сбор активности приложений и сайтов | AWatch-rus развивает этот подход в пилотный корпоративный контур с ролями, отчетами, Risk Narrative и эксплуатационной документацией |
|
||||||
| Стахановец | Контроль сотрудников, мониторинг активности, DLP-возможности | Зрелый классический контроль рабочих мест и политик мониторинга | AWatch-rus не должен заявлять функциональный паритет; его сильная зона - объяснимый управленческий KPI, Security Analytics и пилотная прозрачность |
|
| Стахановец | Контроль сотрудников, мониторинг активности, DLP-возможности | Зрелый классический контроль рабочих мест и политик мониторинга | AWatch-rus не заявляет функциональный паритет; его сильная зона - объяснимый управленческий KPI, Security Analytics и пилотная прозрачность |
|
||||||
| StaffCop | Employee Monitoring, Insider Risk, Workforce Analytics, DLP | Широкий набор функций мониторинга, productivity analytics, расследований и DLP-направления | AWatch-rus нужно показывать как более узкий и прозрачный пилотный контур, без обещания заменить StaffCop по широте функций |
|
| StaffCop | Employee Monitoring, Insider Risk, Workforce Analytics, DLP | Широкий набор функций мониторинга, productivity analytics, расследований и DLP-направления | AWatch-rus это более узкий и прозрачный пилотный контур, без обещания заменить StaffCop по широте функций |
|
||||||
| SearchInform | DLP, Risk Monitor, SIEM, TimeInformer и смежные продукты | Комплексная линейка ИБ-продуктов и мониторинга внутренних рисков | AWatch-rus не конкурирует как полноценный SIEM/DLP; он может быть легким аналитическим слоем для Workforce-first пилота |
|
| SearchInform | DLP, Risk Monitor, SIEM, TimeInformer и смежные продукты | Комплексная линейка ИБ-продуктов и мониторинга внутренних рисков | AWatch-rus не конкурирует как полноценный SIEM/DLP; он является легким аналитическим слоем для Workforce-first пилота |
|
||||||
| InfoWatch | DLP и защита от утечек конфиденциальной информации | Сильное DLP-направление, политики, интеграции и регуляторный контекст | AWatch-rus не заменяет DLP; он показывает операционную активность, объяснимые риски и материалы для внутренней проверки |
|
| InfoWatch | DLP и защита от утечек конфиденциальной информации | Сильное DLP-направление, политики, интеграции и регуляторный контекст | AWatch-rus не заменяет DLP; он показывает операционную активность, объяснимые риски и материалы для внутренней проверки |
|
||||||
|
|
||||||
## Где AWatch-rus уместен
|
## Где AWatch-rus уместен
|
||||||
|
|
||||||
- Быстрый пилот для руководителя, ИБ и эксплуатации без тяжелого SIEM/DLP
|
- Быстрый пилот для руководителя, ИБ и эксплуатации без тяжелого SIEM/DLP
|
||||||
внедрения.
|
внедрения в организациях,желающих иметь современное программное обеспечение такого типа.
|
||||||
- Workforce-first аналитика с объяснением KPI, coverage и confidence.
|
- Workforce-first аналитика с объяснением KPI, coverage и confidence.
|
||||||
- Разделение Executive, Workforce, Security и Forensics сценариев.
|
- Разделение Executive, Workforce, Security и Forensics сценариев.
|
||||||
- Прозрачная rule-based модель UEBA Score v1 и Risk Narrative без ML/LLM.
|
- Прозрачная rule-based модель UEBA Score v1 и Risk Narrative без дорогих средств использования Искусственного Интеллекта ML/LLM.
|
||||||
- Подготовка evidence package и Markdown-отчетов для ручной проверки.
|
- Подготовка evidence package и Markdown-отчетов для ручной проверки.
|
||||||
- Честная демонстрация границ: planned, future и contract_only не выдаются за
|
- Честная демонстрация границ: planned, future и contract_only не выдаются за implemented.
|
||||||
implemented.
|
|
||||||
|
|
||||||
## Где зрелые конкуренты обычно сильнее
|
## Где зрелые тяжелые конкуренты обычно сильнее
|
||||||
|
|
||||||
- Глубокие DLP-политики, контентная фильтрация и блокировки каналов утечки.
|
- Глубокие DLP-политики, контентная фильтрация и блокировки каналов утечки.
|
||||||
- Масштабные SIEM/SOC-процессы и готовые интеграции ИБ.
|
- Масштабные SIEM/SOC-процессы и готовые интеграции ИБ.
|
||||||
@@ -39,12 +38,10 @@ Forensics с прозрачными rule-based объяснениями.
|
|||||||
- Поддержка сложных enterprise-сценариев с централизованным управлением
|
- Поддержка сложных enterprise-сценариев с централизованным управлением
|
||||||
агентами и политиками.
|
агентами и политиками.
|
||||||
|
|
||||||
## Что нельзя заявлять
|
## Что не заявляется
|
||||||
|
|
||||||
- Что AWatch-rus заменяет DLP, SIEM, EDR или XDR.
|
- Что AWatch-rus заменяет DLP, SIEM, EDR или XDR.
|
||||||
- Что planned или future providers уже работают в production.
|
- Что planned или future providers уже работают в production.
|
||||||
- Что pfSense readiness означает готовый ingestion, если он находится в статусе
|
|
||||||
`contract_only`.
|
|
||||||
- Что Risk Narrative является ML-прогнозом.
|
- Что Risk Narrative является ML-прогнозом.
|
||||||
- Что система автоматически оценивает персонал или принимает кадровые решения.
|
- Что система автоматически оценивает персонал или принимает кадровые решения.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# GitHub-сборка Rust-бинарников AWatch-rus
|
||||||
|
|
||||||
|
## Принятое решение
|
||||||
|
|
||||||
|
Для проекта AWatch-rus каноническая release-сборка Rust-бинарников выполняется в GitHub Actions.
|
||||||
|
|
||||||
|
Локальная сборка используется для разработки и предварительной проверки. Официальным источником release-бинарников считаются только artifacts, полученные из GitHub Actions на конкретном commit или tag.
|
||||||
|
|
||||||
|
## Toolchain
|
||||||
|
|
||||||
|
Версия Rust/Cargo фиксируется в `rust-toolchain.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[toolchain]
|
||||||
|
channel = "1.94.0"
|
||||||
|
profile = "minimal"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Workflow должны запускать Cargo явно:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo +1.94.0 --version
|
||||||
|
rustc +1.94.0 --version
|
||||||
|
cargo +1.94.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check
|
||||||
|
cargo +1.94.0 test --manifest-path adk-rust/Cargo.toml --workspace --no-fail-fast
|
||||||
|
cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings
|
||||||
|
cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Это исключает ситуацию, когда GitHub runner использует старый системный Cargo.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Основные workflow:
|
||||||
|
|
||||||
|
- `.github/workflows/rust-workspace.yml` — fmt, tests, clippy, release build всего workspace.
|
||||||
|
- `.github/workflows/rust-professionalization-check.yml` — PR smoke для изменяемых Rust-крейтов.
|
||||||
|
- `.github/workflows/rust-binary-build.yml` — сборка release-бинарников Linux x86_64 и публикация GitHub Actions artifact.
|
||||||
|
|
||||||
|
## rust-binary-build
|
||||||
|
|
||||||
|
Workflow `rust-binary-build` запускается:
|
||||||
|
|
||||||
|
- вручную через GitHub Actions -> rust-binary-build -> Run workflow;
|
||||||
|
- автоматически при push tag вида `v*`.
|
||||||
|
|
||||||
|
Внутри workflow выполняется:
|
||||||
|
|
||||||
|
1. checkout repository;
|
||||||
|
2. установка Rust/Cargo 1.94.0;
|
||||||
|
3. вывод версий `cargo` и `rustc`;
|
||||||
|
4. format check;
|
||||||
|
5. workspace tests;
|
||||||
|
6. workspace clippy;
|
||||||
|
7. workspace release build;
|
||||||
|
8. upload artifact `awatch-rus-linux-x86_64-release-binaries`.
|
||||||
|
|
||||||
|
## Правило проекта
|
||||||
|
|
||||||
|
Перед передачей бинарников на пилот, демонстрацию или релиз нужно использовать GitHub Actions artifact, а не локально собранный файл.
|
||||||
|
|
||||||
|
Минимальные признаки корректного artifact:
|
||||||
|
|
||||||
|
- workflow завершился успешно;
|
||||||
|
- в логах указан Rust/Cargo 1.94.0;
|
||||||
|
- build выполнен из нужного commit или tag;
|
||||||
|
- artifact скачан из GitHub Actions.
|
||||||
|
|
||||||
|
## Дальнейшие улучшения
|
||||||
|
|
||||||
|
Отдельными PR можно добавить:
|
||||||
|
|
||||||
|
- SHA256SUMS для каждого бинарника;
|
||||||
|
- автоматическую публикацию в GitHub Release при tag `v*`;
|
||||||
|
- Windows x86_64 build для endpoint-компонентов;
|
||||||
|
- Linux static/musl build при необходимости;
|
||||||
|
- подпись release artifacts.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.94.0"
|
||||||
|
profile = "minimal"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create a GitHub Actions release package from Rust release binaries."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import tarfile
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SKIP_DIRS = {"deps", "build", "examples", "incremental"}
|
||||||
|
SKIP_SUFFIXES = {".d", ".rlib", ".rmeta"}
|
||||||
|
|
||||||
|
|
||||||
|
def sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
while True:
|
||||||
|
chunk = handle.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def is_binary(path: Path) -> bool:
|
||||||
|
if not path.is_file():
|
||||||
|
return False
|
||||||
|
if path.name in SKIP_DIRS:
|
||||||
|
return False
|
||||||
|
if path.suffix in SKIP_SUFFIXES:
|
||||||
|
return False
|
||||||
|
return bool(path.stat().st_mode & stat.S_IXUSR)
|
||||||
|
|
||||||
|
|
||||||
|
def collect(release_dir: Path) -> list[Path]:
|
||||||
|
items = [item for item in sorted(release_dir.iterdir()) if is_binary(item)]
|
||||||
|
if not items:
|
||||||
|
raise SystemExit(f"No release binaries found in {release_dir}")
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def write(path: Path, text: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_release_file(src: Path, dst: Path) -> Path:
|
||||||
|
"""Copy file contents without preserving metadata that some mounts reject."""
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(src, dst)
|
||||||
|
try:
|
||||||
|
dst.chmod(src.stat().st_mode & 0o777)
|
||||||
|
except PermissionError:
|
||||||
|
# Some removable/network filesystems reject chmod/utime metadata changes.
|
||||||
|
# The package remains valid because the archive manifest/checksums are
|
||||||
|
# based on file contents, not filesystem timestamps.
|
||||||
|
pass
|
||||||
|
return dst
|
||||||
|
|
||||||
|
|
||||||
|
def write_archive_checksum(archive: Path) -> None:
|
||||||
|
write(archive.with_suffix(archive.suffix + ".sha256"), f"{sha256(archive)} {archive.name}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def create_compatibility_aliases(out_dir: Path, archive: Path) -> None:
|
||||||
|
"""Create both linux-x86_64 and linux_x86_64 artifact paths."""
|
||||||
|
out_alias = Path(str(out_dir).replace("linux-x86_64", "linux_x86_64"))
|
||||||
|
if out_alias != out_dir:
|
||||||
|
if out_alias.exists():
|
||||||
|
shutil.rmtree(out_alias)
|
||||||
|
out_alias.mkdir(parents=True)
|
||||||
|
for item in out_dir.iterdir():
|
||||||
|
if item.is_file():
|
||||||
|
copy_release_file(item, out_alias / item.name)
|
||||||
|
|
||||||
|
archive_alias = Path(str(archive).replace("linux-x86_64", "linux_x86_64"))
|
||||||
|
if archive_alias != archive:
|
||||||
|
copy_release_file(archive, archive_alias)
|
||||||
|
write_archive_checksum(archive_alias)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--release-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--out-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--archive", type=Path, required=True)
|
||||||
|
parser.add_argument("--target", default="linux-x86_64")
|
||||||
|
parser.add_argument("--commit", default="unknown")
|
||||||
|
parser.add_argument("--ref", default="unknown")
|
||||||
|
parser.add_argument("--run-id", default="unknown")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
release_dir = args.release_dir.resolve()
|
||||||
|
out_dir = args.out_dir.resolve()
|
||||||
|
archive = args.archive.resolve()
|
||||||
|
|
||||||
|
if out_dir.exists():
|
||||||
|
shutil.rmtree(out_dir)
|
||||||
|
out_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
binaries = collect(release_dir)
|
||||||
|
for binary in binaries:
|
||||||
|
copy_release_file(binary, out_dir / binary.name)
|
||||||
|
|
||||||
|
names = [binary.name for binary in binaries]
|
||||||
|
write(out_dir / "BINARIES.txt", "\n".join(names) + "\n")
|
||||||
|
|
||||||
|
checksum_lines = []
|
||||||
|
manifest_binaries = []
|
||||||
|
for name in names:
|
||||||
|
packaged = out_dir / name
|
||||||
|
digest = sha256(packaged)
|
||||||
|
checksum_lines.append(f"{digest} {name}")
|
||||||
|
manifest_binaries.append(
|
||||||
|
{"name": name, "size_bytes": packaged.stat().st_size, "sha256": digest}
|
||||||
|
)
|
||||||
|
write(out_dir / "SHA256SUMS.txt", "\n".join(checksum_lines) + "\n")
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"project": "AWatch-rus",
|
||||||
|
"target": args.target,
|
||||||
|
"commit": args.commit,
|
||||||
|
"ref": args.ref,
|
||||||
|
"run_id": args.run_id,
|
||||||
|
"build_time_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||||
|
"binaries": manifest_binaries,
|
||||||
|
}
|
||||||
|
write(out_dir / "BUILD_MANIFEST.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
|
||||||
|
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tarfile.open(archive, "w:gz") as tar:
|
||||||
|
tar.add(out_dir, arcname=out_dir.name)
|
||||||
|
write_archive_checksum(archive)
|
||||||
|
create_compatibility_aliases(out_dir, archive)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user