Hợp đồng giao tiếp
Trạng thái: đã cài ở bộ đọc và vai trò matcher; bộ gửi (SCN-4) và vai trò ingest (SMT-6) theo hợp đồng này. Chi tiết đã cài (văn bản chuẩn của inventory_hash, kiểm khung, mã lỗi, kiểm thử chấp nhận) ở contract/security-report-v1.md. Mọi thông điệp thuộc gói protobuf accesshub.scan.v1 của repo này (proto/accesshub/scan/v1/report.proto). Access Hub Scanner không đi qua Access Hub Agent hay Access Hub Collector (quyết định Q25, ADR 0001). Các quy ước chung lấy theo kiểu của giám sát nhưng là bản riêng của Scanner: TLS, header X-AHS-Proto, X-AHS-Uploader-Version, X-Request-Id, mã lỗi dạng {"error":{"code","message"}}, backoff full jitter.
1. Tổng quan các chặng
| Chặng | Kênh | Định dạng | Xác thực |
|---|---|---|---|
| Bộ đọc sang bộ gửi | Tệp trong spool /var/lib/accesshub-scanner/spool/ | Khung AHSR + protobuf nén gzip | Quyền tệp (nhóm accesshub-scan-uploader chỉ đọc) |
| Bộ gửi sang bộ đọc (cấu hình) | Tệp /var/lib/accesshub-scan-uploader/config.pb | Protobuf SecurityConfig | Quyền tệp |
| Bộ gửi sang ingest (enroll) | POST /scan/v1/enroll | JSON | Mã enroll Scanner một lần dùng |
| Bộ gửi sang ingest (báo cáo) | POST /scan/v1/report | Protobuf SecurityReport | Chữ ký Ed25519 của máy Scanner (mục 3.0) |
| Bộ gửi sang ingest (cấu hình) | GET /scan/v1/config | Protobuf SecurityConfig, ETag | Như trên |
| Ingest sang matcher | POST /internal/v1/reports (mạng nội bộ) | Protobuf ForwardedReport | Bearer token ingest riêng của matcher |
| Ingest sang Access Hub | /api/v1/security/matcher/scan-hosts/... (mục 3.0, 8) | JSON | Sanctum token của tài khoản dịch vụ có quyền security.matcher |
| Matcher sang Access Hub | POST /api/v1/security/matcher/events | JSON theo API v1 | Như trên |
| Access Hub sang matcher | /internal/v1/... (mạng nội bộ) | JSON | Bearer token admin riêng của secmatch |
Danh tính company_id, server_id, host_id chỉ do ingest gắn từ registry máy Scanner theo khóa đã ký request. Bộ đọc và bộ gửi không gửi các trường này trong báo cáo; nếu có thì bị bỏ. Máy cài cả giám sát có thêm agent_id riêng của giám sát; hai danh tính độc lập, chỉ gặp nhau ở bản ghi Server của Access Hub.
2. Spool trên máy
2.1 Tệp báo cáo
- Tên
<scanned_at_ms 13 chữ số>-<report_id>-<part_index>.ahsr, ghi nguyên tử (tệp tạm.tmp-*cùng thư mục,fsync, đổi tên,fsyncthư mục). Thư mụcaccesshub-scanner:accesshub-scan-uploader, chế độ2750; tệp0640. - Khung: 4 byte
AHSR, 4 byte độ dài header (big endian), header là protobufSpoolHeader, phần còn lại là body:SecurityReport(kiểuFULL) đã nén gzip, gửi được nguyên trạng vớiContent-Encoding: gzip. SpoolHeader { report_id, schema_version, scanned_at_ms, inventory_hash, checks_hash, body_sha256, body_bytes, part_index, part_count, content_hash }(content_hashtừschema_version2).- Scanner giữ tối đa 2 bộ báo cáo, xóa bộ cũ nhất khi ghi bộ mới. Bộ gửi chỉ đọc (không xóa), ghi
report_idđã được xác nhận vào/var/lib/accesshub-scan-uploader/state.json.
2.2 Bộ gửi xử lý spool
- Mỗi 60 giây liệt kê spool, chọn bộ mới nhất có đủ
part_counttệp, chưa được xác nhận. - Kiểm tra khung: đúng magic,
schema_versiontrong miền bộ gửi biết là có thể chuyển (1),body_byteskhớp kích thước,body_sha256khớp. Sai thì bỏ qua tệp và ghi trạng tháispool_corrupt(không gửi). - Nếu
inventory_hash,checks_hashvàcontent_hashtrùng bộ đã được xác nhận gần nhất: gửiSecurityReportkiểuHASH_ONLYdựng từ header (vài trăm byte). Nếu không: gửi body nguyên trạng. - Ack có
need_full = true: gửi lại bodyFULLcủa cùng bộ.
Bộ gửi không giải nén và không phân tích body. Bộ đọc và bộ gửi cùng gói nên luôn cùng phiên bản; schema_version 2 được bật cùng lúc ở cả hai.
3. Bộ gửi sang ingest
3.0 Enroll, danh tính, giấy phép
- Người có quyền
security.hosts.managetạo mã enroll Scanner trong Access Hub (một lần dùng, hết hạn 24 giờ, theo công ty, tùy chọn gắn sẵn với mộtServer). Access Hub chỉ cấp khi công ty có tính năngsecurity_scanningvà còn chỗ trong hạn mứcscan_hosts. - Trên máy:
accesshub-scan-uploader enroll --url https://<ingest> --token-file <tệp>. Bộ gửi sinh cặp khóa Ed25519 tại chỗ (khóa riêng0600trong/var/lib/accesshub-scan-uploader/), gửiPOST /scan/v1/enroll{"token", "public_key", "machine_id_sha256", "hostname", "os": {"id", "version_id"}, "uploader_version"}. - Ingest gọi Access Hub
POST /api/v1/security/matcher/scan-hosts/enrollđể đổi mã lấy bản ghiscan_hosts(Access Hub kiểm mã, giấy phép, gắnServertheo mã hoặc theomachine_id_sha256khớp máy đã có, hoặc tạoServermới), nhận{host_id, company_id, server_id}, trả bộ gửi201 {"host_id"}. Mã sai, hết hạn, đã dùng: 401invalid_enrolment; hết giấy phép hoặc hạn mức: 403licence_inactive. - Mỗi request sau đó mang
Authorization: AHS-Ed25519 host_id=<id>, ts=<unix ms>, sig=<base64>, chữ ký trênmethod \n path \n ts \n sha256(body). Ingest từ chối lệch giờ quá 5 phút và chữ ký lặp lại trong cửa sổ đó (401bad_signature). - Ingest giữ bản sao registry máy Scanner (
GET /api/v1/security/matcher/scan-hosts?since=, mỗi 60 giây và khi enroll) gồm khóa công khai, trạng thái,company_id,server_id, trạng thái giấy phép của công ty. Máy bị thu hồi hoặc công ty hết gói, vượt hạn mức: 403licence_inactive. - Xoay khóa: bộ gửi gọi
POST /scan/v1/rotateký bằng khóa cũ, gửi khóa công khai mới; khóa cũ hết hiệu lực sau 24 giờ. Gỡ gói (purge) xóa khóa; máy trong Access Hub cần được thu hồi riêng.
Đã cài (2026-10-03): internal/scanauth (ký, xác minh, chống lặp), internal/ingest (vai trò ingest, lệnh accesshub-secmatch ingest), internal/uploader (lệnh accesshub-scan-uploader). Chi tiết chốt khi cài:
- Chuỗi ký:
METHOD \n PATH \n TS \n hex(sha256(body)),PATHlà đường dẫn URL không có query, body là đúng byte trên dây (đã nén nếuContent-Encoding: gzip). URL ingest không được có tiền tố đường dẫn bị proxy cắt (chữ ký sẽ sai). Khóa công khai trên dây là base64 chuẩn của 32 byte Ed25519; khóa riêng lưu PKCS#8 PEM0600, bộ gửi từ chối khóa nhóm hoặc người khác đọc được. POST /scan/v1/rotatebody{"public_key"}ký bằng khóa cũ, trả200 {"status":"rotated"}; khóa cũ còn hiệu lực 24 giờ.GET /scan/v1/configtrảSecurityConfigprotobuf vớiETaglà 16 byte đầu SHA-256 (hex) của bản mã hóa xác định;If-None-Matchkhớp thì 304.- Dạng API ở mục này được chủ dự án duyệt nguyên văn ngày 2026-10-03 (Q45). Bổ sung cộng thêm ngày 2026-10-03 (theo yêu cầu phía Access Hub): siêu dữ liệu máy và
scan_windowdưới đây. - Siêu dữ liệu máy (chỉ để nhận diện, không bí mật):
hostname,primary_ipv4,primary_ipv6,os {id, version_id},machine_id_sha256,uploader_version. Gửi kèmPOST /scan/v1/enroll(hai trường địa chỉ mới, tùy chọn) và làm mới bằngPUT /scan/v1/host(ký như mọi request, body JSON đúng các trường trên, trường lạ bị từ chối, 204 khi xong, tối đa 12 lần mỗi giờ mỗi máy, burst 4). Bộ gửi làm mới khi giá trị đổi và ít nhất mỗi 24 giờ. Địa chỉ chính là địa chỉ nguồn máy dùng để tới ingest (bộ gửi lấy bằng UDPconnectkhông gửi gói nào, nên không cần netlink).- Giới hạn (ingest kiểm ở cả enroll và làm mới, sai thì 400):
hostnamerỗng hoặc tên kiểu DNS tối đa 253 byte ([A-Za-z0-9._-], đầu và cuối là chữ hoặc số);primary_ipv4là IPv4 dạng chữ, không phải loopback, link-local, multicast, 0.0.0.0;primary_ipv6là IPv6 dạng chữ, không phải IPv4 ánh xạ, loopback, link-local, multicast, không có zone;os.id,os.version_id,uploader_versiontối đa 64 ký tự[A-Za-z0-9._+~-];machine_id_sha256đúng 64 ký tự hex thường. Bộ gửi tự để trống trường tùy chọn không hợp lệ trước khi gửi, để không chặn enroll. - Ingest chuyển cho Access Hub:
POST /api/v1/security/matcher/scan-hosts/{host_id}/metadata, body JSON như trên; 2xx là xong. Ở enroll, các trường này nằm trong bodyscan-hosts/enrollnhư cũ (thêmprimary_ipv4,primary_ipv6).
- Giới hạn (ingest kiểm ở cả enroll và làm mới, sai thì 400):
- Giới hạn tốc độ (báo cáo theo máy, enroll theo địa chỉ) và khử trùng 24 giờ giữ trong bộ nhớ của từng bản ingest (Q46, chấp nhận 2026-10-03): N bản sau load balancer cho giới hạn hiệu lực tới N lần, khử trùng không thấy request đã vào bản khác, và mất khi khởi động lại. An toàn vì matcher bỏ báo cáo không mới hơn (
scanned_at_ms) và ghép phần theoreport_id. Khi chạy quá 2 bản hoặc phép đo B5 (15) cho thấy cần, chuyển hai bảng này sang kho dùng chung (Redis, khóa theohost_id). - Enroll giới hạn 20 lần mỗi giờ mỗi địa chỉ nguồn (burst 5), 429 kèm
Retry-After. Body JSON tối đa 16 KiB, trường lạ bị từ chối,machine_id_sha256là 64 ký tự hex. - API Access Hub mà ingest gọi (tài khoản dịch vụ
security-matcher,Authorization: Bearer), dạng JSON chốt cho HUB-SEC-21:POST /api/v1/security/matcher/scan-hosts/enroll, body nhưPOST /scan/v1/enroll; 201{"data": {"host_id", "company_id", "server_id", "public_key", "state", "licence_active", "version"}}; 401, 404, 410: mã sai, hết hạn, đã dùng; 402, 403: không có gói hoặc hết hạn mức.server_idcó thểnull(bổ sung 2026-10-03): khi máy Scanner chưa gắn vớiServernào ("Chưa gắn máy chủ"), Access Hub trảserver_id: nullở enroll, ở registry và ở trả lời làm mới siêu dữ liệu. Ingest vẫn nhận và chuyển báo cáo; matcher lưu và so khớp theohost_id(khóa ổn định,finding_keykhông đổi khi gắn sau). Sự kiện gửi Access Hub manghost_idvàserver_id: nullcho tới khi gắn. Khi Access Hub gắn máy, lần đồng bộ registry kế tiếp (mỗi phút) lấyserver_id; báo cáo kế tiếp của máy (cảHASH_ONLY) mang nó tới matcher, và từ đó sự kiện cóserver_id. Truy vấn/internal/v1/servers/{id}/...nhận cảserver_idđã gắn lẫnhost_id, luôn trong phạm vicompany_id.GET /api/v1/security/matcher/scan-hosts?since=<version>:{"data": {"version": n, "hosts": [{"host_id", "company_id", "server_id", "public_key", "prev_public_key", "prev_key_expires_at", "state" ("active", "revoked"), "licence_active", "scan_requested_at", "version"}]}}, chỉ bản ghi đổi sausince. Bản ghi cócompany_id,server_idkhông khớp^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$bị bỏ qua.POST /api/v1/security/matcher/scan-hosts/{host_id}/rotatebody{"public_key"}; 2xx là xong.GET /api/v1/security/matcher/settings/{company_id}:{"data": {"security": {...}}}như mục 8; thiếusecuritynghĩa là tắt.
3.1 POST /scan/v1/report
Content-Type: application/x-protobuf, Content-Encoding: gzip (hoặc không nén cho HASH_ONLY). Thông điệp thuộc gói accesshub.scan.v1 của repo này, tuân thủ quy tắc tương thích (không đổi số trường, không tái dùng số đã xóa, kiểm bằng buf breaking).
message SecurityReport {
string report_id = 1; // UUIDv7, do scanner sinh
uint32 schema_version = 2; // 1
string scanner_version = 3;
int64 scanned_at_ms = 4;
ReportKind kind = 5;
string inventory_hash = 6; // SHA-256 hex của inventory chuẩn hóa (gói sắp xếp)
string checks_hash = 7; // SHA-256 hex của kết quả kiểm tra chuẩn hóa
uint32 part_index = 8; // từ 0
uint32 part_count = 9; // 1 khi không chia
OsRelease os = 10;
KernelInfo kernel = 11;
repeated Package packages = 12; // rỗng khi HASH_ONLY
repeated CheckResult checks = 13; // rỗng khi HASH_ONLY
repeated ListeningSocket listening = 14;
ScanStats stats = 15;
}
enum ReportKind { REPORT_KIND_UNSPECIFIED = 0; REPORT_KIND_FULL = 1; REPORT_KIND_HASH_ONLY = 2; }
message OsRelease { string id = 1; string version_id = 2; string codename = 3; repeated string id_like = 4; string variant = 5; }
message KernelInfo { string running = 1; repeated string installed = 2; bool reboot_required = 3; bool livepatch_active = 4; }
enum PackageManager { PACKAGE_MANAGER_UNSPECIFIED = 0; PACKAGE_MANAGER_DPKG = 1; PACKAGE_MANAGER_RPM = 2; PACKAGE_MANAGER_APK = 3; }
message Package {
PackageManager manager = 1; string name = 2; string version = 3; string epoch = 4; string release = 5;
string arch = 6; string source_name = 7; string source_version = 8; string vendor = 9; string module = 10;
}
enum CheckStatus { CHECK_STATUS_UNSPECIFIED = 0; CHECK_STATUS_PASS = 1; CHECK_STATUS_FAIL = 2; CHECK_STATUS_ERROR = 3; CHECK_STATUS_NOT_APPLICABLE = 4; CHECK_STATUS_UNKNOWN = 5; }
message CheckResult {
string check_id = 1; uint32 check_version = 2; CheckStatus status = 3; string subject = 4;
map<string, string> evidence = 5; // khóa thuộc danh sách trắng của check_id
string error_code = 6;
}
message ListeningSocket { string proto = 1; string address = 2; uint32 port = 3; }
message ScanStats {
uint32 duration_ms = 1; uint32 cpu_ms = 2; uint64 max_rss_bytes = 3;
uint32 files_walked = 4; bool walk_truncated = 5; repeated string error_codes = 6;
}
message SecurityReportAck { string report_id = 1; bool accepted = 2; bool need_full = 3; int64 server_time_ms = 4; }3.2 Giới hạn (ingest kiểm tra, bộ đọc tuân thủ trước khi ghi)
| Giới hạn | Giá trị | Vượt thì |
|---|---|---|
| Body sau nén, sau giải nén | 1 MiB, 8 MiB (như mọi endpoint) | 413 |
| Số gói mỗi phần | 20.000 | 413; scanner chia phần trước |
| Số phần mỗi báo cáo | 8 | 413 |
Số CheckResult | 500 | 413 |
evidence | Tối đa 20 khóa, khóa ^[a-z][a-z0-9_]{0,31}$, giá trị tối đa 256 byte UTF-8 | Cắt giá trị, bỏ khóa thừa, cảnh báo |
ListeningSocket | 1.024 | Cắt, cảnh báo |
| Chuỗi tên, phiên bản gói | Tối đa 256 byte, tên và phiên bản khác rỗng | 400 cả phần. Không bỏ riêng gói: sửa danh sách gói làm lệch inventory_hash |
check_id | ^AHS-[A-Z]{2,5}-[0-9]{3}$ | Kết quả bị bỏ |
scanned_at_ms | Không cũ hơn 7 ngày, không quá 5 phút ở tương lai | 422 stale_report |
| Tốc độ | 6 request mỗi giờ mỗi máy Scanner, burst 8 (đủ cho báo cáo 8 phần) | 429 kèm Retry-After |
Giới hạn evidence là lớp phòng thủ thứ hai cho ADR 0006; lớp thứ nhất là danh sách trắng trong mã scanner.
3.4 Phần mở rộng schema_version 2: nội dung và kiểm kê mở rộng
Thêm vào SecurityReport (số trường chốt khi làm P2). Báo cáo v2 luôn mang ảnh chụp đầy đủ finding nội dung hiện biết của máy, dựng từ chỉ mục cục bộ (ADR 0011 mục 5), không phải chỉ kết quả của lượt vừa chạy.
message SecurityReport {
// ... trường 1 đến 15 như mục 3.1
string content_hash = 16; // SHA-256 hex của phần nội dung chuẩn hóa
repeated ContentFinding content = 17; // rỗng khi HASH_ONLY
ContentScanStats content_stats = 18;
repeated Service services = 19; // P6
repeated SoftwareItem software = 20; // P6
repeated RootFileStats file_stats = 21;
repeated NotableFile notable_files = 22;
}
enum ContentCategory { CONTENT_CATEGORY_UNSPECIFIED = 0; CONTENT_CATEGORY_SECRET = 1; CONTENT_CATEGORY_SENSITIVE_DATA = 2; }
enum Severity { SEVERITY_UNSPECIFIED = 0; SEVERITY_INFO = 1; SEVERITY_LOW = 2; SEVERITY_MEDIUM = 3; SEVERITY_HIGH = 4; SEVERITY_CRITICAL = 5; }
enum Confidence { CONFIDENCE_UNSPECIFIED = 0; CONFIDENCE_LOW = 1; CONFIDENCE_MEDIUM = 2; CONFIDENCE_HIGH = 3; }
enum Exposure { EXPOSURE_UNSPECIFIED = 0; EXPOSURE_OWNER_ONLY = 1; EXPOSURE_GROUP_READABLE = 2; EXPOSURE_WORLD_READABLE = 3; EXPOSURE_WEB_ROOT = 4; EXPOSURE_HISTORY = 5; }
message ContentFinding {
string detector_id = 1; // AHK-... hoặc AHP-...
uint32 detector_version = 2;
ContentCategory category = 3;
string path = 4;
uint32 line = 5;
uint32 column = 6; // 0 với dữ liệu cá nhân
string preview = 7; // bí mật: tối đa 4 đầu + "***" + 2 cuối; dữ liệu cá nhân: luôn rỗng
string fingerprint = 8; // "h<v>:<hex64>" hoặc "c<v>:<hex64>"
Severity severity = 9;
int64 first_seen_ms = 10;
int64 last_seen_ms = 11;
int64 file_modified_ms = 12;
uint32 match_count = 13; // Q29
Confidence confidence = 14; // Q29
Exposure exposure = 15; // Q29
}
message DetectorCount { string detector_id = 1; uint32 count = 2; }
message ContentScanStats {
string rules_version = 1; bool secrets_enabled = 2; bool pii_enabled = 3;
uint64 files_eligible = 4; uint64 files_scanned = 5; uint64 bytes_read = 6;
uint64 skipped_too_large = 7; uint64 skipped_binary = 8; uint64 skipped_error = 9;
bool baseline_complete = 10; int64 last_run_started_ms = 11; uint32 last_run_cpu_ms = 12;
uint32 last_run_pauses = 13; string last_run_stop_reason = 14; // window_end, cpu_budget, byte_budget, entry_budget, pressure, done
bool findings_truncated = 15; repeated DetectorCount truncated_counts = 16;
}
message Service { string name = 1; string unit_file_state = 2; string active_state = 3; string package = 4; }
message SoftwareItem { string kind = 1; string name = 2; string version = 3; string path = 4; string purl = 5; string sha256 = 6; }
message RootFileStats {
string root = 1; uint64 files = 2; uint64 bytes = 3; uint64 eligible = 4;
uint64 world_readable = 5; uint64 world_writable = 6; map<string, uint64> by_kind = 7;
}
message NotableFile {
string path = 1; string kind = 2; uint64 size = 3; uint32 mode = 4; uint32 uid = 5; uint32 gid = 6;
int64 modified_ms = 7; Exposure exposure = 8;
}Giới hạn bổ sung (ingest kiểm tra, lớp thứ hai của ADR 0011; sai ở một finding thì bỏ finding đó, tăng secmatch_ingest_content_rejected_total{reason}, không bỏ cả báo cáo):
| Trường | Ràng buộc |
|---|---|
detector_id | ^AH[KP]-[A-Z]{2,5}-[0-9]{3}$; AHK đi với SECRET, AHP đi với SENSITIVE_DATA |
preview | Rỗng, hoặc khớp ^[\x21-\x7e]{0,4}\*\*\*[\x21-\x7e]{0,2}$ (tối đa 9 byte). Bắt buộc rỗng khi SENSITIVE_DATA |
fingerprint | ^[hc][0-9]{1,3}:[0-9a-f]{64}$; SENSITIVE_DATA chỉ được phạm vi h |
path | Tuyệt đối, tối đa 1.024 byte UTF-8 hợp lệ |
| Số lượng | content tối đa 2.000 mục SECRET và 5.000 mục SENSITIVE_DATA; services tối đa 2.000; software tối đa 20.000; notable_files tối đa 2.000; file_stats tối đa 64 |
| Kích thước | Phần nội dung tối đa 256 KB sau nén (SNFR-18); toàn báo cáo vẫn theo giới hạn 3.2, chia phần nếu cần |
3.3 Mã trạng thái và hành vi bộ gử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ộ báo cáo 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ờ (có thể máy đã bị thu hồi hoặc cần 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 cũ chưa có endpoint: tạm dừng 1 giờ, trạng thái ingest_unsupported | |
| 413 | too_large | Không tự chia (scanner đã chia), bỏ bộ, trạng thái report_too_large |
| 422 | unsupported_schema, stale_report | Giữ tệp, không gửi lại bộ này, trạng thái tương ứng; bộ mới của scanner sẽ được thử |
| 429 | rate_limited | Theo Retry-After |
| 5xx, lỗi mạng | Backoff full jitter (tối đa 1 giờ), giữ bộ báo cáo |
4. Khử trùng, idempotency, thứ tự
- Khóa idempotent ở ingest:
(host_id, report_id, part_index), cache 24 giờ. Gửi lặp trả 202 như lần đầu. - Secmatch ghép phần theo
(server_id, report_id), đủpart_countmới xử lý, phần lẻ quá 1 giờ thì bỏ. - Secmatch chỉ nhận báo cáo có
scanned_at_msmới hơn bản đang lưu của máy; báo cáo cũ hơn đến muộn trảacceptednhưng không đổi gì. HASH_ONLYmà secmatch không có bản khớpinventory_hashcủa máy (mất dữ liệu, máy mới chuyển công ty): ingest trả ngay ackneed_full = truenếu biết, nếu không matcher đánh dấu và ingest trảneed_fullở lần sau (ingest giữ cờ theohost_id).- Sự kiện gửi Access Hub mang
event_id = SHA-256(finding_key | transition_seq),seqtăng chặt theo máy. Access Hub khử trùng theoevent_idvà bỏ sự kiện cóseqnhỏ hơnseqđã áp của finding.
5. Ingest sang matcher
POST /internal/v1/reports, body ForwardedReport { company_id, server_id, host_id, received_at_ms, SecurityReport report }. Ingest ghi vào outbox bền vững trước khi trả 202 cho bộ gửi, gửi lô, thử lại khi 5xx. Secmatch trả 202 hoặc 4xx vĩnh viễn (bỏ vào dead-letter, có metric).
6. Secmatch sang Access Hub
POST /api/v1/security/matcher/events, tối đa 200 sự kiện, idempotent theo event_id, phản hồi {"data":{"accepted":[...],"rejected":[{"event_id","reason"}]}} như mẫu events của monitoring. Access Hub kiểm tra server_id thuộc company_id và máy Scanner (scan_hosts) của máy đó không ở trạng thái revoked; sai thì từ chối unknown_server (secmatch chuyển dead-letter, không thử lại).
type | Payload chính |
|---|---|
finding.opened | finding_key, kind (vulnerability, check, secret, sensitive_data), vuln_id, aliases, advisory_ids, ecosystem, package_key, packages[] (tên, phiên bản đã cài), fixed_version, fix_status (fixed, no_fix, wont_fix), severity_label, cvss_score, cvss_vector, score_source, epss, epss_percentile, kev, kev_due_date, reboot_required, first_seen_at, db_version |
finding.updated | finding_key, các trường đổi (điểm, EPSS, KEV, fixed_version, fix_status, packages) |
finding.resolved | finding_key, resolved_reason (package_updated, package_removed, advisory_changed, check_passed, content_removed, file_deleted, out_of_scope, fingerprint_reset), resolved_at |
check.state | check_id, check_version, subject, status, evidence, observed_at (cho cả pass, error, unknown để hiển thị độ phủ) |
server.scanned | scanned_at, scanner_version, db_version, os, kernel_running, package_count, walk_truncated, error_codes, listening[], content_stats (độ phủ, rules_version, baseline_complete) |
Với kind là secret hoặc sensitive_data, payload của finding.opened và finding.updated chỉ gồm finding_key, kind, các trường của ContentFinding (ADR 0011 mục 2) và db_version rỗng. Secmatch không thêm trường nào khác.
Access Hub tính risk, priority, SLA từ các trường trên cùng thuộc tính Server (06 mục 1.5).
7. Access Hub sang secmatch (/internal/v1, mạng nội bộ)
Mọi lời gọi truyền company_id tường minh, lấy từ bản ghi Server hoặc phiên người dùng, không lấy từ tham số người dùng. Secmatch tự ràng buộc truy vấn theo company_id.
| Method, đường dẫn | Ý nghĩa |
|---|---|
GET /internal/v1/servers/{server_id}/packages?company_id=&q=&cursor=&limit= | Danh sách gói của một máy, tìm theo tên, phân trang con trỏ |
GET /internal/v1/companies/{company_id}/packages/search?name=&version_lt=&version_gte=&ecosystem=&cursor= | Máy nào có gói X trong khoảng phiên bản (so theo quy tắc của hệ) |
GET /internal/v1/companies/{company_id}/summary | Số máy có inventory, tuổi báo cáo cũ nhất, phân bố distro |
DELETE /internal/v1/companies/{company_id}/data | Xóa inventory, finding nội bộ, trạng thái của công ty (idempotent, có bia mộ như COL-11) |
DELETE /internal/v1/servers/{server_id}/data?company_id= | Xóa của một máy |
POST /internal/v1/resync?company_id= | Phát lại toàn bộ finding đang mở của công ty (khôi phục sau sự cố Access Hub) |
GET /internal/v1/status | Phiên bản, db_version, db_created_at, db_stale, độ sâu outbox, tiến độ so khớp lại |
GET /internal/v1/stats/global | Số tổng hợp ẩn danh cho nền tảng (không company_id, áp ngưỡng k) |
8. Cấu hình quét (Access Hub sang ingest sang bộ gửi sang bộ đọc)
Ingest lấy cài đặt quét theo công ty từ Access Hub (GET /api/v1/security/matcher/settings/{company_id}, đã kẹp theo gói, có version), ghép giá trị theo máy (scan_requested_at) từ registry máy Scanner, dựng SecurityConfig cho từng host_id. Bộ gửi gọi GET /scan/v1/config với If-None-Match mỗi 5 phút; 304 thì giữ nguyên, 200 thì ghi nguyên tử config.pb. Khóa security có dạng (thiếu nghĩa là tắt):
{"security": {"enabled": true, "inventory_interval_hours": 24, "checks_interval_hours": 24,
"checks_enabled": true, "disabled_checks": ["AHS-FS-004"], "walk_exclude": ["/opt/legacy"],
"thresholds": {"AHS-SSH-006.max_auth_tries": 4, "AHS-AUTH-003.max_days": 365}, "max_walk_files": 200000,
"scan_requested_at": null}}Từ P2 thêm khóa con content (thiếu nghĩa là tắt quét nội dung):
{"security": {"content": {"secrets_enabled": true, "pii_enabled": false,
"profiles": ["system-config", "web", "home"], "custom_roots": [], "exclude": ["/srv/cache", "vendor"],
"quiet_window": {"start": "01:00", "end": "05:00"},
"max_file_mib": {"secrets": 4, "pii": 128}, "read_budget_mib_per_run": 1024, "cpu_budget_seconds_per_run": 300,
"disabled_detectors": ["AHK-JWT-001"], "pii_count_thresholds": [100, 1000], "pii_email_min_count": 5,
"fp_company_key": {"version": 3, "key_b64": "..."}}}}fp_company_key là bí mật (ADR 0011 mục 3): Access Hub giải mã từ ENC-2 chỉ khi trả cài đặt cho ingest, ingest không log và không lưu ngoài bộ nhớ đệm cấu hình, bộ gửi ghi config.pb với quyền 0640 (bộ đọc đọc bằng CAP_DAC_READ_SEARCH). Kiểm tra bổ sung: hồ sơ thuộc danh sách cố định, custom_roots tối đa 16 đường dẫn tuyệt đối không .. và không thuộc danh sách cấm cứng, exclude tối đa 64 mục, ngân sách và ngưỡng trong miền của ADR 0009, mã detector thuộc danh mục. Scanner áp thêm local.conf (quyết định cục bộ thắng).
Khóa scan_window (bổ sung 2026-10-03; thiếu hoặc null nghĩa là quét bất cứ lúc nào):
{"security": {"scan_window": {"start": "01:00", "end": "05:00", "timezone": "Asia/Ho_Chi_Minh",
"utc_offset": "+07:00", "days": ["mon", "tue", "wed", "thu", "fri"]}}}start,end: giờ địa phươngHH:MM.endtrướcstartlà khung qua nửa đêm và thuộc ngày bắt đầu:days: ["mon"]với22:00đến02:00là từ 22:00 thứ Hai tới 02:00 thứ Ba.endbằngstartlà cả ngày. Đầu khung tính, cuối khung không tính.timezone(tên IANA, ưu tiên) hoặcutc_offset(±HH:MM, tối đa 14 giờ); thiếu cả hai là UTC. Bộ đọc có sẵn dữ liệu múi giờ, không phụ thuộc máy.days:sun,mon,tue,wed,thu,fri,sat; rỗng là mọi ngày.- Ingest chuyển thành
SecurityConfig.scan_window(start_minute,end_minute,timezone,utc_offset_minutes,days0 là Chủ nhật). Khung sai định dạng thì ingest bỏ khung (ghi log) và bộ đọc cũng bỏ khung sai (đếmconfig_values_dropped). - Bộ đọc áp khung: timer vẫn chạy mỗi giờ, nhưng ngoài khung thì một lần quét đến hạn chỉ ghi trạng thái
outside_windowvà chờ; vào khung thì quét. Yêu cầu quét sớm (scan_requested_atmới hơn lần quét đầy đủ cuối) vượt khung đúng một lần.accesshub-scanner scan --forcecủa quản trị viên máy luôn quét. Bộ gửi không quét nên không bị khung chặn: nó gửi báo cáo đã có bất cứ lúc nào và mang khung mới xuống máy quaconfig.pb.
scan_requested_at (RFC3339 hoặc null) là gợi ý lịch cho nút "Quét ngay" nếu Q11 được chấp nhận: scanner thấy giá trị mới hơn lần quét cuối thì quét đầy đủ ở lần timer kế tiếp. Access Hub giới hạn 1 lần mỗi giờ mỗi máy. Không phải lệnh: nó chỉ dời lịch của việc vốn sẽ chạy. Giá trị theo máy đi qua registry máy Scanner (Access Hub nâng version của bản ghi scan_hosts), ingest ghép vào SecurityConfig của đúng host_id đó.
ETag đổi khi giá trị đổi. Kiểm tra ở cả ba nơi (Access Hub khi lưu, ingest khi phân phối, bộ đọc khi đọc): mã kiểm tra phải thuộc danh mục, đường dẫn loại trừ là đường dẫn tuyệt đối không chứa .., tối đa 32 mục, ngưỡng nằm trong miền của từng kiểm tra, chu kỳ không nhỏ hơn trần của gói. Khóa lạ bị bỏ. enabled = false (gói không có tính năng hoặc công ty tắt) thì bộ đọc chỉ ghi trạng thái, không quét.
9. Phiên bản hóa
- Thông điệp thuộc
accesshub.scan.v1vàaccesshub.secmatch.v1của repo này, kiểm bằngbuf breakingtrong CI của repo này. Endpoint/scan/v1chỉ thay đổi bổ sung. schema_versioncủa báo cáo do secmatch hỗ trợ N và N-1 tối thiểu 12 tháng (SNFR-44). Ingest có cấu hình miềnschema_versionchấp nhận để trả 422 đồng bộ.- API
/internal/v1của secmatch và/api/v1/security/matchercủa Access Hub chỉ thay đổi bổ sung; đổi ngữ nghĩa thì tăngv.