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.
Tệp nguồn:
| Tệp | Nội dung |
|---|---|
proto/accesshub/scan/v1/report.proto (gói accesshub.scan.v1) | SecurityReport, SpoolHeader, SecurityConfig, SecurityReportAck |
proto/accesshub/secmatch/v1/secmatch.proto | ForwardedReport (mang host_id), ForwardedReportAck |
internal/spool, internal/inventory | Cà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ệc | Bộ gửi (SCN-4) | Ingest (SMT-6) | Matcher |
|---|---|---|---|
| Enroll, giữ khóa máy Scanner | Có | Đổi mã enroll với Access Hub | |
| Đọc và kiểm khung spool | Có | ||
Gửi FULL hoặc HASH_ONLY | Có | ||
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ùng | Có | Kiểm lại (lớp hai) | |
Ghép phần, kiểm inventory_hash | Không | Có | |
| Phân phối cấu hình quét | Ghi config.pb | GET /scan/v1/config | |
| Ma trận hai tenant, fuzz | Fuzz khung spool | Mụ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ómaccesshub-scan-uploader, chế độ2750(setgid, tệp mới thuộc nhóm bộ gửi). Tệp0640. - 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_idcó 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 (SpoolHeaderprotobuf), phần còn lại là body (SecurityReportkiểuFULL, 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,fsyncthư 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;statschỉ ở phần 0. Không vừa 8 phần thì bộ đọc không ghi gì và ghi mãreport_too_largevà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:
- 4 byte đầu là
AHSR. - Độ dài header từ 1 đến 4.096 và không vượt kích thước tệp.
- Header giải mã được thành
SpoolHeader. - Số byte body bằng
body_bytes. - SHA-256 (hex chữ thường) của body bằng
body_sha256. schema_versionlà1(bản P2 của cả gói bật thêm2).
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_hashvàchecks_hashcủa bộ mới bằng bộ đã được ingest xác nhận gần nhất: gửi mộtSecurityReportkiểuHASH_ONLYdự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ôngos,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ầnFULLcủ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.installedgiữ thứ tự bộ đọc gửi (đã sắp). os.id_like,os.variant,stats,report_id,scanned_at_mskhô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
cddd98f9fcfb0239e7e92f5f43cc00586cab231d0d8a6a7075e5aa8704f59d65Ingest 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: gzipvớiFULL, không nén vớiHASH_ONLY.Authorization: AHS-Ed25519 host_id=<id>, ts=<unix ms>, sig=<base64>(enroll, chữ ký, xoay khóa:07mục 3.0) và các headerX-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 tra | Giá 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ại | 401 bad_signature |
| Giấy phép | Máy active, công ty có security_scanning, trong hạn mức scan_hosts | 403 licence_inactive |
| Body nén, giải nén | 1 MiB, 8 MiB | 413 too_large |
schema_version | 1 | 422 unsupported_schema |
scanned_at_ms | Không cũ hơn 7 ngày, không quá 5 phút ở tương lai, so với giờ ingest | 422 stale_report |
inventory_hash | ^sha256:[0-9a-f]{64}$ | 400 bad_request |
report_id | 1 đến 64 byte | 400 |
part_count, part_index | 1 ≤ part_count ≤ 8, part_index < part_count | 400 |
| Số gói mỗi phần | Tối đa 20.000 | 413 |
| Gói | name, version khác rỗng; name, version, source_name, source_version, arch tối đa 256 byte | 400 (không bỏ riêng gói, xem mục 3) |
os.id, os.version_id, os.codename, kernel.running, scanner_version | Tối đa 256 byte | 400 |
| 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_mslà 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ờ theohost_id(TTL 7 ngày) và trảneed_full = truetrong ack của request kế tiếp từ máy đó, rồi xóa cờ. Nếu ingest giữ đượcinventory_hashgần nhất theo máy, có thể trảneed_fullngay choHASH_ONLYkhông khớp.
4.4 Mã trạng thái
| HTTP | code | Bộ gửi làm gì |
|---|---|---|
| 202 | Ghi report_id đã xác nhận; need_full thì gửi FULL ngay | |
| 400 | bad_request | Bỏ bộ này, trạng thái report_rejected |
| 401 | bad_signature, unknown_host | Dừ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) |
| 403 | licence_inactive | Ngừng gửi, giữ spool, trạng thái licence_inactive, thử lại sau 6 giờ |
| 404 | Ingest chưa có endpoint: dừng 1 giờ, trạng thái ingest_unsupported | |
| 413 | too_large | Bỏ bộ, trạng thái report_too_large (bộ gửi không tự chia) |
| 422 | unsupported_schema, stale_report | Không gửi lại bộ này, trạng thái tương ứng |
| 429 | rate_limited | Theo Retry-After |
| 5xx, lỗi mạng | Backoff full jitter tối đa 1 giờ, giữ bộ |
5. Ingest sang matcher: POST /internal/v1/reports
- Body
ForwardedReportprotobuf, 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_mskhô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_unreadable | Không đọc được /etc/os-release hay /usr/lib/os-release |
dpkg_status_missing | Không có /var/lib/dpkg/status |
dpkg_status_unreadable | Có nhưng không đọc được |
dpkg_status_too_large | Vượt giới hạn kích thước của parser |
dpkg_entries_dropped | Có mục hỏng bị bỏ qua |
dpkg_too_many_packages | Vượt số gói tối đa |
kernel_release_unreadable | Không đọc được phiên bản kernel đang chạy |
livepatch_unreadable | Không đọc được trạng thái livepatch |
no_supported_package_db | Khô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},07mục 8), ghép giá trị theo máy từ registry máy Scanner, dựngSecurityConfigcho từnghost_id. Thiếu khóasecuritynghĩa làenabled = false. - Bộ gửi gọi
GET /scan/v1/config(ký như mục 4) vớiIf-None-Matchmỗi 5 phút; 200 thì ghiSecurityConfignguyê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ằngCAP_DAC_READ_SEARCH. - Bộ đọc kiểm lại: chu kỳ 1 đến 168 giờ (mặc định 24),
disabled_checkskhớp^AHS-[A-Z]{2,5}-[0-9]{3}$,walk_excludetối đa 32 đường dẫn tuyệt đối không..,max_walk_filestố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)
- Hai tenant: hai máy Scanner của hai công ty gửi cùng
report_idvà cùng gói; mỗiForwardedReportmang đúng danh tính của khóa đã ký; khóa idempotent không trùng chéo. - 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_idcủ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. - 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. - 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.
- Băm nguyên vẹn: báo cáo đi qua bộ gửi, ingest tới matcher có
inventory_hashkhớp (dùng fixturetestdata/fixtures/*quaaccesshub-scanner scan --root). - 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.
- 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.
need_full:HASH_ONLYcho máy chưa có inventory: lần gửi kế tiếp bộ gửi nhậnneed_full = true.- Độc lập: máy không cài
accesshub-agentvẫ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.