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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

### 추가

- **출력 로케일 (한국어·영문).** 모든 사람용 출력(CLI 도움말, 배너와 한계 목록, 승인 프롬프트, 감사 보고, 마법사, 생성되는 정책 파일의 주석)이 로케일을 따름. 결정 순서는 `AIRLOCK_LANG` -> `~/.config/airlock/config.toml`의 `locale` 키 -> `LC_ALL`/`LC_MESSAGES`/`LANG` 접두 -> 기본 한국어. 기계 판독 필드(JSON 키, `kind`·`status`·`verdict`, 스키마 이름, 규칙 id)와 **정책 다이제스트는 로케일과 무관하게 고정**임. 자세한 것은 `docs/i18n.md`
- **`airlock setup` 첫 질문이 언어 선택.** 답하는 즉시 나머지 질문이 그 언어로 나가고 선택이 구성 파일에 저장됨. 영문 프리셋(`examples/policy/en/*.toml`)이 추가되었으며 한국어판과 주석만 다르고 다이제스트가 같음을 테스트가 강제함
- `airlock audit report --json` 최상위에 `locale` 필드 추가. 이상 `detail` 산문이 로케일을 따르므로 리포트 본문 다이제스트가 생성 로케일에 묶이며, `report`와 `ack`는 같은 로케일에서 돌려야 함 (`docs/limitations.md` 7.18)
- **`airlock audit report`. 매일 이상여부 점검.** 범위 안의 모든 세션을 검증하고 결정·승인·거부된 exec·목적지별 아웃바운드를 집계해 한 번에 보고함. `--since`/`--until`은 그 날을 통째로 덮고, `--json`은 색상 코드 없는 안정된 스키마(`airlock.audit-report.v1`)로 나감. **이상이 하나라도 있으면 종료 코드가 비영**이므로 cron 이나 launchd 에 그대로 걸 수 있음. 종료 코드는 0 이상 없음, 2 증거 이상, 3 운영 이상, 64 인자 오류, 70 내부 오류임
- **판단 불가를 통과로 보고하지 않음.** 앵커 없음, 세션을 읽지 못함, 확인 체인 손상, **앵커에는 있는데 디렉토리가 없는 세션**을 전부 증거 이상으로 셈. 마지막 항목이 세션 통째 삭제를 잡으며, 남은 세션만 훑는 보고로는 그것이 "이상 없음" 으로 나옴
- **승인 집계가 사람 신원 있는 응답과 없는 응답을 나눔.** `--yes` 자동 승인은 `approver_uid`·`approver_tty`가 둘 다 없으므로 여기서 갈리며, 사람 확인처럼 보이지 않음. `--strict-approval`은 신원 없이 **허용된** 건만 이상으로 셈. 신원 없는 거부는 아무것도 통과시키지 않았으므로 세지 않음
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ repository = "https://github.com/Quant-Off/airlock"
airlock-audit = { path = "crates/airlock-audit", version = "0.1.0" }
airlock-broker = { path = "crates/airlock-broker", version = "0.1.0" }
airlock-canonical = { path = "crates/airlock-canonical", version = "0.1.0" }
airlock-i18n = { path = "crates/airlock-i18n", version = "0.1.0" }
airlock-policy = { path = "crates/airlock-policy", version = "0.1.0" }
airlock-proxy = { path = "crates/airlock-proxy", version = "0.1.0" }
airlock-setup = { path = "crates/airlock-setup", version = "0.1.0" }
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ $ cp examples/policy/strict.toml airlock.toml

The semantics of the policy system are written up in [policy-dsl.md](docs/policy-dsl.md).

All human-facing output is available in Korean (default) and English. `airlock setup` asks for the language first and saves the choice to `~/.config/airlock/config.toml`; `AIRLOCK_LANG=en` overrides it per run. English preset variants live in `examples/policy/en/`. Details in [i18n.md](docs/i18n.md).

Enforcing host level egress policy for real requires `--egress-proxy`. Without this flag, the host list in a policy is only a declaration of intent.

```bash
Expand Down
2 changes: 2 additions & 0 deletions README_KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ $ cp examples/policy/strict.toml airlock.toml

정책 시스템에 관한 의미론적 문서를 [policy-dsl.md](docs/policy-dsl.md)에 정리해 두었습니다.

모든 사람용 출력은 한국어(기본)와 영문을 지원합니다. `airlock setup`의 첫 질문이 언어 선택이며 선택은 `~/.config/airlock/config.toml`에 저장됩니다. 일회성 전환은 `AIRLOCK_LANG=en`으로 합니다. 영문 프리셋은 `examples/policy/en/`에 있습니다. 자세한 내용은 [i18n.md](docs/i18n.md)를 참고하세요.

호스트 단위 egress 정책을 실제로 강제하려면 `--egress-proxy`가 필요합니다. 이 플래그가 없으면 정책의 호스트 목록은 의도 선언에 그칩니다.

```bash
Expand Down
1 change: 1 addition & 0 deletions crates/airlock-audit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ description = "Airlock의 해시체인 append-only 감사 로그와 검증기"

[dependencies]
airlock-canonical.workspace = true
airlock-i18n.workspace = true
libc.workspace = true
sha2.workspace = true
serde.workspace = true
Expand Down
156 changes: 131 additions & 25 deletions crates/airlock-audit/src/anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};

use airlock_canonical::Encoder;
use airlock_i18n::tr;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

Expand Down Expand Up @@ -300,47 +301,114 @@ impl fmt::Display for AnchorFailure {
match self {
Self::FileAbsent => write!(
f,
"{ANCHOR_FILE} 없음. 세션 삭제와 체인 재계산을 탐지할 수 없음. 앵커 삭제 의심"
"{}",
tr!(
format!(
"{ANCHOR_FILE} 없음. 세션 삭제와 체인 재계산을 탐지할 수 없음. 앵커 삭제 의심"
),
format!(
"{ANCHOR_FILE} missing; session deletion and chain recomputation cannot be detected. Suspected anchor deletion"
)
)
),
Self::ChainEmpty => write!(f, "앵커 체인이 비어 있음"),
Self::MalformedLine { line, detail } => {
write!(f, "앵커 {line}번째 줄 파싱 실패: {detail}")
}
Self::TruncatedFinalLine { line } => {
write!(f, "앵커 {line}번째 줄이 개행 없이 잘림. 쓰기 중 중단 의심")
Self::ChainEmpty => {
f.write_str(tr!("앵커 체인이 비어 있음", "the anchor chain is empty"))
}
Self::MalformedLine { line, detail } => write!(
f,
"{}",
tr!(
format!("앵커 {line}번째 줄 파싱 실패: {detail}"),
format!("failed to parse anchor line {line}: {detail}")
)
),
Self::TruncatedFinalLine { line } => write!(
f,
"{}",
tr!(
format!("앵커 {line}번째 줄이 개행 없이 잘림. 쓰기 중 중단 의심"),
format!(
"anchor line {line} is truncated without a newline; suspected interruption mid-write"
)
)
),
Self::BlankLine { line } => write!(
f,
"앵커 {line}번째 줄이 비어 있음. 앵커가 아닌 줄이 끼어들었음"
"{}",
tr!(
format!("앵커 {line}번째 줄이 비어 있음. 앵커가 아닌 줄이 끼어들었음"),
format!("anchor line {line} is blank; a non-anchor line crept in")
)
),
Self::FormatVersionUnsupported { seq, got } => write!(
f,
"앵커 seq {seq}의 포맷이 v{got}. 이 검증자는 v{ANCHOR_VERSION}만 안다. 변조가 아니라 옛 포맷일 수 있으니 그 버전의 airlock으로 검증할 것"
"{}",
tr!(
format!(
"앵커 seq {seq}의 포맷이 v{got}. 이 검증자는 v{ANCHOR_VERSION}만 안다. 변조가 아니라 옛 포맷일 수 있으니 그 버전의 airlock으로 검증할 것"
),
format!(
"anchor seq {seq} has format v{got}; this verifier only knows v{ANCHOR_VERSION}. It may be an old format rather than tampering, so verify with that version of airlock"
)
)
),
Self::GenesisPrevNotZero { got } => {
write!(f, "첫 앵커의 prev가 0이 아님: {got}")
}
Self::SeqGap { expected, got } => {
write!(
f,
"앵커 seq 빈틈. {expected} 기대, {got} 발견. 앵커 줄 삭제 의심"
Self::GenesisPrevNotZero { got } => write!(
f,
"{}",
tr!(
format!("첫 앵커의 prev가 0이 아님: {got}"),
format!("the first anchor's prev is not zero: {got}")
)
}
),
Self::SeqGap { expected, got } => write!(
f,
"{}",
tr!(
format!("앵커 seq 빈틈. {expected} 기대, {got} 발견. 앵커 줄 삭제 의심"),
format!(
"anchor seq gap; expected {expected}, found {got}. Suspected anchor line deletion"
)
)
),
Self::PrevMismatch { seq, expected, got } => write!(
f,
"앵커 seq {seq}의 prev 불일치. {expected} 기대, {got} 발견. 삭제·삽입·재배치 의심"
"{}",
tr!(
format!(
"앵커 seq {seq}의 prev 불일치. {expected} 기대, {got} 발견. 삭제·삽입·재배치 의심"
),
format!(
"anchor seq {seq} prev mismatch; expected {expected}, found {got}. Suspected deletion, insertion, or reordering"
)
)
),
Self::HashMismatch { seq, expected, got } => write!(
f,
"앵커 seq {seq}의 hash 불일치. 재계산 {expected}, 기록 {got}. 내용 변조 의심"
"{}",
tr!(
format!(
"앵커 seq {seq}의 hash 불일치. 재계산 {expected}, 기록 {got}. 내용 변조 의심"
),
format!(
"anchor seq {seq} hash mismatch; recomputed {expected}, recorded {got}. Suspected content tampering"
)
)
),
Self::SessionDuplicated {
seq,
session,
first_seq,
} => write!(
f,
"앵커 seq {seq}가 세션 {session}을 같은 head_seq로 다시 기록함 (처음은 seq {first_seq}). 앵커 줄 복제 의심"
"{}",
tr!(
format!(
"앵커 seq {seq}가 세션 {session}을 같은 head_seq로 다시 기록함 (처음은 seq {first_seq}). 앵커 줄 복제 의심"
),
format!(
"anchor seq {seq} records session {session} again with the same head_seq (first at seq {first_seq}); suspected anchor line duplication"
)
)
),
Self::HeadSeqRegressed {
seq,
Expand All @@ -349,7 +417,15 @@ impl fmt::Display for AnchorFailure {
got,
} => write!(
f,
"앵커 seq {seq}에서 세션 {session}의 head_seq가 되감김 ({previous} -> {got}). 세션 체인은 자라기만 하므로 잘라내기 의심"
"{}",
tr!(
format!(
"앵커 seq {seq}에서 세션 {session}의 head_seq가 되감김 ({previous} -> {got}). 세션 체인은 자라기만 하므로 잘라내기 의심"
),
format!(
"head_seq of session {session} regressed at anchor seq {seq} ({previous} -> {got}); a session chain only grows, so truncation is suspected"
)
)
),
Self::SessionHeadMismatch {
session,
Expand All @@ -362,12 +438,28 @@ impl fmt::Display for AnchorFailure {
if chain_head_seq > anchor_head_seq {
write!(
f,
"세션 {session}의 체인이 앵커보다 김. 앵커(seq {anchor_seq})는 head {anchor_head_seq}, 체인은 head {chain_head_seq}. 앵커 이후 덧붙이기 의심"
"{}",
tr!(
format!(
"세션 {session}의 체인이 앵커보다 김. 앵커(seq {anchor_seq})는 head {anchor_head_seq}, 체인은 head {chain_head_seq}. 앵커 이후 덧붙이기 의심"
),
format!(
"the chain of session {session} is longer than the anchor; the anchor (seq {anchor_seq}) says head {anchor_head_seq}, the chain says head {chain_head_seq}. Suspected append after anchoring"
)
)
)
} else {
write!(
f,
"세션 {session}의 head가 앵커와 다름. 앵커(seq {anchor_seq})는 {anchor_head_seq}/{anchor_head_hash}, 체인은 {chain_head_seq}/{chain_head_hash}. 체인 재계산 의심"
"{}",
tr!(
format!(
"세션 {session}의 head가 앵커와 다름. 앵커(seq {anchor_seq})는 {anchor_head_seq}/{anchor_head_hash}, 체인은 {chain_head_seq}/{chain_head_hash}. 체인 재계산 의심"
),
format!(
"the head of session {session} differs from the anchor; the anchor (seq {anchor_seq}) says {anchor_head_seq}/{anchor_head_hash}, the chain says {chain_head_seq}/{chain_head_hash}. Suspected chain recomputation"
)
)
)
}
}
Expand All @@ -389,11 +481,25 @@ impl fmt::Display for AnchorWarning {
match self {
Self::ClockWentBackwards { seq, prev_ts, ts } => write!(
f,
"앵커 seq {seq}에서 벽시계가 역행 ({prev_ts} -> {ts}). 시각 조정 가능성"
"{}",
tr!(
format!(
"앵커 seq {seq}에서 벽시계가 역행 ({prev_ts} -> {ts}). 시각 조정 가능성"
),
format!(
"wall clock went backwards at anchor seq {seq} ({prev_ts} -> {ts}); possible clock adjustment"
)
)
),
Self::SessionReanchored { session, count } => write!(
f,
"세션 {session}이 {count}번 앵커됨. 중간 체크포인트로 판단"
"{}",
tr!(
format!("세션 {session}이 {count}번 앵커됨. 중간 체크포인트로 판단"),
format!(
"session {session} anchored {count} times; judged to be intermediate checkpoints"
)
)
),
}
}
Expand Down
Loading
Loading