Access Hub Scanner
Đang phát triểnĐồng bộ từ mã nguồn lúc 10:57, 03/10/2026
Skip to content

Hợp đồng SecurityReport v1 (bộ gửi SCN-4 và vai trò ingest SMT-6) ​

Trạng thái: đã cài ở bộ đọc accesshub-scanner và vai trò matcher của secmatch; bộ gửi accesshub-scan-uploader (SCN-4) và vai trò ingest (SMT-6) phải theo đúng tài liệu này. Cả bốn thành phần thuộc repo này. Access Hub Scanner không đi qua Access Hub Agent hay Access Hub Collector (quyết định Q25, ADR 0001). Tài liệu mô tả đúng hành vi của mã ở commit hiện tại; chỗ nào khác docs/07-wire-contract.md thì tài liệu này đúng và 07 phải được sửa theo.

10 phút đọcCập nhật 03/10/2026access-hub-scanner, docs/contract/security-report-v1.md

Tệp nguồn:

TệpNội dung
proto/accesshub/scan/v1/report.proto (gói accesshub.scan.v1)SecurityReport, SpoolHeader, SecurityConfig, SecurityReportAck
proto/accesshub/secmatch/v1/secmatch.protoForwardedReport (mang host_id), ForwardedReportAck
internal/spool, internal/inventoryCài đặt tham chiếu của khung spool và băm inventory; bộ gửi dùng trực tiếp spool.Decode

1. Các chặng và ai làm gì ​

bộ đọc --(tệp .ahsr trong spool)--> bộ gửi --(POST /scan/v1/report, ký Ed25519)--> ingest
ingest --(POST /internal/v1/reports, ForwardedReport)--> matcher --(sự kiện)--> Access Hub
Access Hub --(settings, registry máy Scanner)--> ingest --(GET /scan/v1/config)--> bộ gửi --(config.pb)--> bộ đọc
ViệcBộ gửi (SCN-4)Ingest (SMT-6)Matcher
Enroll, giữ khóa máy ScannerCóĐổi mã enroll với Access Hub
Đọc và kiểm khung spoolCó
Gửi FULL hoặc HASH_ONLYCó
Gắn danh tính (company_id, server_id, host_id)Từ registry máy Scanner theo chữ kýKhông bao giờ tin payload
Kiểm giấy phép (security_scanning, hạn mức scan_hosts)Có (403 licence_inactive)
Giới hạn kích thước, tốc độ, khử trùngCóKiểm lại (lớp hai)
Ghép phần, kiểm inventory_hashKhôngCó
Phân phối cấu hình quétGhi config.pbGET /scan/v1/config
Ma trận hai tenant, fuzzFuzz khung spoolMục 8Đã có (internal/secmatch)

2. Spool trên máy ​

  • Thư mục mặc định /var/lib/accesshub-scanner/spool, chủ accesshub-scanner, nhóm accesshub-scan-uploader, chế độ 2750 (setgid, tệp mới thuộc nhóm bộ gửi). Tệp 0640.
  • Tên tệp: <scanned_at_ms, 13 chữ số>-<report_id>-<part_index>.ahsr, ví dụ 1790000000000-0192a3b4-...-0.ahsr. Bộ gửi phân tích tên bằng dấu - đầu tiên và cuối cùng (report_id có thể chứa -). Tệp bắt đầu bằng .tmp- là tệp đang ghi: bỏ qua.
  • Khung: 4 byte ASCII AHSR, 4 byte độ dài header (uint32 big endian, 1 đến 4.096), header (SpoolHeader protobuf), phần còn lại là body (SecurityReport kiểu FULL, protobuf nén gzip). Kích thước tệp tối đa 4.096 + 1 MiB + 8 byte.
  • Ghi nguyên tử: tệp tạm cùng thư mục, fsync, đổi tên, fsync thư mục. Bộ gửi không bao giờ thấy tệp dở.
  • Giữ lại: bộ đọc giữ 2 bộ báo cáo mới nhất (mọi phần), xóa bộ cũ và tệp tạm quá 1 giờ. Bộ gửi không xóa tệp spool.
  • Chia phần: tối đa 8 phần, mỗi phần tối đa 20.000 gói và body nén tối đa 1 MiB. Mọi phần lặp lại report_id, schema_version, scanned_at_ms, inventory_hash, os, kernel; stats chỉ ở phần 0. Không vừa 8 phần thì bộ đọc không ghi gì và ghi mã report_too_large vào trạng thái của nó.

2.1 Bộ gửi kiểm khung ​

Theo thứ tự, sai bước nào thì bỏ tệp đó, trạng thái spool_corrupt, không gửi:

  1. 4 byte đầu là AHSR.
  2. Độ dài header từ 1 đến 4.096 và không vượt kích thước tệp.
  3. Header giải mã được thành SpoolHeader.
  4. Số byte body bằng body_bytes.
  5. SHA-256 (hex chữ thường) của body bằng body_sha256.
  6. schema_version là 1 (bản P2 của cả gói bật thêm 2).

Chỉ gửi một bộ khi có đủ part_count tệp cùng report_id. Bộ gửi không giải nén, không phân tích body. Hàm spool.Decode là cài đặt tham chiếu, bộ gửi gọi trực tiếp.

2.2 FULL hay HASH_ONLY ​

  • Nếu inventory_hash và checks_hash của bộ mới bằng bộ đã được ingest xác nhận gần nhất: gửi một SecurityReport kiểu HASH_ONLY dựng từ header: report_id, schema_version, scanned_at_ms, inventory_hash, checks_hash, kind = REPORT_KIND_HASH_ONLY, part_index = 0, part_count = 1, không os, kernel, packages. Không nén.
  • Nếu không: gửi từng phần, body nguyên trạng với Content-Encoding: gzip.
  • Ack có need_full = true: gửi lại các phần FULL của cùng bộ ngay (không chờ chu kỳ).

3. inventory_hash: văn bản chuẩn ​

inventory_hash = "sha256:" + hex chữ thường của SHA-256(văn bản chuẩn). Văn bản chuẩn gồm các dòng kết thúc bằng \n, trường cách nhau bởi \t:

os\t<os.id>\t<os.version_id>\t<os.codename>\n
kernel\t<kernel.running>\t<kernel.installed nối bằng ",">\t<reboot_required 0|1>\t<livepatch_active 0|1>\n
pkg\t<manager số enum>\t<name>\t<arch>\t<version>\t<source_name>\t<source_version>\t<epoch>\t<release>\t<vendor>\t<module>\n   (mỗi gói một dòng)
  • Gói sắp theo (name, arch, version) so sánh byte. Thứ tự gói trong báo cáo không ảnh hưởng băm.
  • Băm tính trên toàn bộ gói của mọi phần; mỗi phần mang cùng giá trị.
  • Trường vắng là chuỗi rỗng. kernel.installed giữ thứ tự bộ đọc gửi (đã sắp).
  • os.id_like, os.variant, stats, report_id, scanned_at_ms không vào băm: hai lần quét cùng inventory cho cùng băm.

Vector kiểm thử (cũng là golden trong internal/inventory/inventory_test.go):

printf 'os\tubuntu\t24.04\tnoble\nkernel\t6.8.0-1-generic\t\t0\t0\npkg\t1\ta\t\t1\ta\t1\t\t\t\t\npkg\t1\tb\t\t1\tb\t1\t\t\t\t\n' | sha256sum
cddd98f9fcfb0239e7e92f5f43cc00586cab231d0d8a6a7075e5aa8704f59d65

Ingest không cần tính băm. Hệ quả quan trọng: ingest không được sửa danh sách gói (bỏ gói, cắt chuỗi). Báo cáo bị sửa sẽ lệch băm và bị matcher từ chối cả bộ. Gói vi phạm giới hạn thì từ chối cả phần bằng 400 (mục 4.2), không bỏ riêng gói.

3b. Máy chưa gắn Server ​

server_id trong registry máy Scanner có thể null (máy "Chưa gắn máy chủ"). Ingest không đòi server_id: danh tính là host_id (từ khóa đã ký) và company_id. ForwardedReport.server_id để trống, matcher lưu theo host_id, sự kiện có host_id và server_id: null. Khi Access Hub gắn máy, báo cáo kế tiếp mang server_id và các sự kiện sau dùng nó; finding_key không đổi. Chi tiết ở 07 mục 3.0.

3a. Siêu dữ liệu máy ​

Bộ gửi gửi siêu dữ liệu nhận diện máy khi enroll và bằng PUT /scan/v1/host khi đổi hoặc mỗi 24 giờ: hostname, primary_ipv4, primary_ipv6, os {id, version_id}, machine_id_sha256 (SHA-256 của /etc/machine-id, không bao giờ giá trị gốc), uploader_version. Không có trường nào khác; trường lạ bị ingest từ chối. Giới hạn từng trường và đường chuyển sang Access Hub ở 07 mục 3.0. Canary (internal/canary/uploader_test.go) kiểm cả request này: giá trị gốc của machine-id không rời máy.

4. Bộ gửi sang ingest: POST /scan/v1/report ​

  • Content-Type: application/x-protobuf; Content-Encoding: gzip với FULL, không nén với HASH_ONLY.
  • Authorization: AHS-Ed25519 host_id=<id>, ts=<unix ms>, sig=<base64> (enroll, chữ ký, xoay khóa: 07 mục 3.0) và các header X-AHS-Proto, X-AHS-Uploader-Version, X-Request-Id.
  • Phản hồi 202 với body SecurityReportAck { report_id, accepted, need_full, server_time_ms } (protobuf).

4.1 Danh tính và giấy phép ​

Ingest lấy host_id từ header đã ký, tra registry máy Scanner (bản sao từ Access Hub) ra khóa công khai, company_id, server_id, trạng thái. SecurityReport không có các trường danh tính; nếu một bản sau thêm trường lạ, ingest bỏ qua. Máy bị thu hồi, công ty không còn tính năng security_scanning hoặc vượt hạn mức scan_hosts: 403 licence_inactive. Máy cài cả giám sát có agent_id riêng của giám sát; ingest không biết và không dùng nó.

4.2 Kiểm tra ở ingest ​

Matcher kiểm lại tất cả các điều dưới đây (lớp hai); ingest kiểm để từ chối sớm và trả mã đúng cho bộ gửi.

Kiểm traGiá trị (khớp hằng trong mã)Sai thì
Chữ kýĐúng khóa của host_id, ts lệch tối đa 5 phút, không lặp lại401 bad_signature
Giấy phépMáy active, công ty có security_scanning, trong hạn mức scan_hosts403 licence_inactive
Body nén, giải nén1 MiB, 8 MiB413 too_large
schema_version1422 unsupported_schema
scanned_at_msKhông cũ hơn 7 ngày, không quá 5 phút ở tương lai, so với giờ ingest422 stale_report
inventory_hash^sha256:[0-9a-f]{64}$400 bad_request
report_id1 đến 64 byte400
part_count, part_index1 ≤ part_count ≤ 8, part_index < part_count400
Số gói mỗi phầnTối đa 20.000413
Góiname, version khác rỗng; name, version, source_name, source_version, arch tối đa 256 byte400 (không bỏ riêng gói, xem mục 3)
os.id, os.version_id, os.codename, kernel.running, scanner_versionTối đa 256 byte400
Tốc độ6 request mỗi giờ mỗi máy Scanner, burst 8 (đủ cho báo cáo 8 phần)429 rate_limited, Retry-After

4.3 Khử trùng và chuyển tiếp ​

  • Khóa idempotent (host_id, report_id, part_index), giữ 24 giờ. Gửi lặp trả lại ack lần đầu.
  • Ingest ghi ForwardedReport { company_id, server_id, host_id, received_at_ms, report } vào outbox bền vững rồi mới trả 202 cho bộ gửi; gửi tới matcher bất đồng bộ, thử lại khi 5xx hoặc lỗi mạng.
  • received_at_ms là giờ ingest nhận request của bộ gửi (không phải giờ chuyển tiếp). Matcher dùng nó để từ chối báo cáo nhận trước khi công ty bị xóa (bia mộ), nên phải giữ nguyên qua thử lại.
  • need_full: vì chuyển tiếp là bất đồng bộ, ingest chưa biết kết quả lúc trả 202. Khi matcher trả need_full = true, ingest đặt cờ theo host_id (TTL 7 ngày) và trả need_full = true trong ack của request kế tiếp từ máy đó, rồi xóa cờ. Nếu ingest giữ được inventory_hash gần nhất theo máy, có thể trả need_full ngay cho HASH_ONLY không khớp.

4.4 Mã trạng thái ​

HTTPcodeBộ gửi làm gì
202Ghi report_id đã xác nhận; need_full thì gửi FULL ngay
400bad_requestBỏ bộ này, trạng thái report_rejected
401bad_signature, unknown_hostDừng gửi, trạng thái auth_failed, thử lại sau 1 giờ (máy có thể đã bị thu hồi: enroll lại)
403licence_inactiveNgừng gửi, giữ spool, trạng thái licence_inactive, thử lại sau 6 giờ
404Ingest chưa có endpoint: dừng 1 giờ, trạng thái ingest_unsupported
413too_largeBỏ bộ, trạng thái report_too_large (bộ gửi không tự chia)
422unsupported_schema, stale_reportKhông gửi lại bộ này, trạng thái tương ứng
429rate_limitedTheo Retry-After
5xx, lỗi mạngBackoff full jitter tối đa 1 giờ, giữ bộ

5. Ingest sang matcher: POST /internal/v1/reports ​

  • Body ForwardedReport protobuf, có thể Content-Encoding: gzip. Authorization: Bearer <ingest token của matcher>, mạng nội bộ.
  • 202 với ForwardedReportAck { report_id, accepted, need_full } (protobuf).
  • 400 bad_request (báo cáo sai, danh tính sai định dạng ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$, báo cáo nhận trước khi công ty bị xóa, băm lệch): lỗi vĩnh viễn, chuyển dead-letter, có metric, không thử lại.
  • 413, 422: như mục 4.4, vĩnh viễn.
  • 401: cấu hình sai token, cảnh báo vận hành, giữ trong outbox.
  • 5xx, 503 not_ready (matcher chưa nạp DB lỗ hổng): thử lại có backoff.
  • Thứ tự giữa các phần không bắt buộc. Matcher ghép phần theo (company_id, server_id, report_id), phần lẻ quá 1 giờ thì bỏ. Báo cáo có scanned_at_ms không mới hơn bản đang lưu được nhận (accepted) nhưng không làm gì.

6. Mã lỗi trong ScanStats.error_codes ​

Chỉ là mã, không bao giờ có văn bản lỗi thô. Ingest chuyển nguyên trạng; Access Hub hiển thị theo bảng dịch.

MãNghĩa
os_release_unreadableKhông đọc được /etc/os-release hay /usr/lib/os-release
dpkg_status_missingKhông có /var/lib/dpkg/status
dpkg_status_unreadableCó nhưng không đọc được
dpkg_status_too_largeVượt giới hạn kích thước của parser
dpkg_entries_droppedCó mục hỏng bị bỏ qua
dpkg_too_many_packagesVượt số gói tối đa
kernel_release_unreadableKhông đọc được phiên bản kernel đang chạy
livepatch_unreadableKhông đọc được trạng thái livepatch
no_supported_package_dbKhông có trình quản lý gói được hỗ trợ (P1: chỉ dpkg)

Mã chỉ có trong trạng thái cục bộ, không đi qua mạng: bộ đọc report_too_large, spool_write_failed; bộ gửi spool_corrupt, report_rejected, auth_failed, licence_inactive, ingest_unsupported.

7. Cấu hình quét ​

  • Ingest lấy cài đặt quét theo công ty từ Access Hub (GET /api/v1/security/matcher/settings/{company_id}, 07 mục 8), ghép giá trị theo máy từ registry máy Scanner, dựng SecurityConfig cho từng host_id. Thiếu khóa security nghĩa là enabled = false.
  • Bộ gửi gọi GET /scan/v1/config (ký như mục 4) với If-None-Match mỗi 5 phút; 200 thì ghi SecurityConfig nguyên trạng, nguyên tử ra /var/lib/accesshub-scan-uploader/config.pb (tối đa 64 KiB, 0640); 304 thì giữ nguyên. Bộ đọc đọc tệp bằng CAP_DAC_READ_SEARCH.
  • Bộ đọc kiểm lại: chu kỳ 1 đến 168 giờ (mặc định 24), disabled_checks khớp ^AHS-[A-Z]{2,5}-[0-9]{3}$, walk_exclude tối đa 32 đường dẫn tuyệt đối không .., max_walk_files tối đa 1.000.000 (mặc định 200.000). Giá trị sai bị bỏ và đếm; tệp hỏng thì dùng mặc định.

8. Kiểm thử chấp nhận cho ingest (SMT-6) và bộ gửi (SCN-4) ​

  1. Hai tenant: hai máy Scanner của hai công ty gửi cùng report_id và cùng gói; mỗi ForwardedReport mang đúng danh tính của khóa đã ký; khóa idempotent không trùng chéo.
  2. Giả danh tính: khóa máy của công ty A ký báo cáo có trường lạ khai company_id của B: trường bị bỏ, báo cáo thuộc A. Mã enroll của A không cho máy vào B.
  3. Giấy phép: máy bị thu hồi, công ty hết gói, vượt hạn mức: 403 licence_inactive; enroll vượt hạn mức bị từ chối.
  4. Giới hạn: mỗi dòng của bảng 4.2 có một ca vượt và một ca biên.
  5. Băm nguyên vẹn: báo cáo đi qua bộ gửi, ingest tới matcher có inventory_hash khớp (dùng fixture testdata/fixtures/* qua accesshub-scanner scan --root).
  6. Fuzz: body ngẫu nhiên, gzip bom, protobuf lồng sâu, header chữ ký hỏng không làm ingest lỗi hay vượt bộ nhớ; khung spool ngẫu nhiên không làm bộ gửi gửi đi.
  7. Nhiều phần: báo cáo 3 phần gửi đảo thứ tự, một phần gửi lặp: matcher nhận đúng một inventory.
  8. need_full: HASH_ONLY cho máy chưa có inventory: lần gửi kế tiếp bộ gửi nhận need_full = true.
  9. Độc lập: máy không cài accesshub-agent vẫn enroll, gửi, nhận cấu hình đầy đủ; gỡ gói Scanner không đụng tệp, tài khoản của Agent.

Matcher đã có kiểm thử tương ứng cho các mục 1, 4 (lớp hai), 7, 8 trong internal/secmatch/engine_test.go và store_test.go.

Trang này có giúp được bạn không?
Sửa trang này

Nội dung đồng bộ từ kho mã access-hub-scanner lúc 10:57, 03/10/2026. Khi tài liệu và mã khác nhau, mã thắng.