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 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.

22 phút đọcCập nhật 03/10/2026access-hub-scanner, docs/07-wire-contract.md

1. Tổng quan các chặng ​

ChặngKênhĐịnh dạngXác thực
Bộ đọc sang bộ gửiTệp trong spool /var/lib/accesshub-scanner/spool/Khung AHSR + protobuf nén gzipQuyề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.pbProtobuf SecurityConfigQuyền tệp
Bộ gửi sang ingest (enroll)POST /scan/v1/enrollJSONMã enroll Scanner một lần dùng
Bộ gửi sang ingest (báo cáo)POST /scan/v1/reportProtobuf SecurityReportChữ ký Ed25519 của máy Scanner (mục 3.0)
Bộ gửi sang ingest (cấu hình)GET /scan/v1/configProtobuf SecurityConfig, ETagNhư trên
Ingest sang matcherPOST /internal/v1/reports (mạng nội bộ)Protobuf ForwardedReportBearer token ingest riêng của matcher
Ingest sang Access Hub/api/v1/security/matcher/scan-hosts/... (mục 3.0, 8)JSONSanctum token của tài khoản dịch vụ có quyền security.matcher
Matcher sang Access HubPOST /api/v1/security/matcher/eventsJSON theo API v1Như trên
Access Hub sang matcher/internal/v1/... (mạng nội bộ)JSONBearer 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, fsync thư mục). Thư mục accesshub-scanner:accesshub-scan-uploader, chế độ 2750; tệp 0640.
  • Khung: 4 byte AHSR, 4 byte độ dài header (big endian), header là protobuf SpoolHeader, phần còn lại là body: SecurityReport (kiểu FULL) đã nén gzip, gửi được nguyên trạng với Content-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_hash từ schema_version 2).
  • 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 ​

  1. Mỗi 60 giây liệt kê spool, chọn bộ mới nhất có đủ part_count tệp, chưa được xác nhận.
  2. Kiểm tra khung: đúng magic, schema_version trong miền bộ gửi biết là có thể chuyển (1), body_bytes khớp kích thước, body_sha256 khớp. Sai thì bỏ qua tệp và ghi trạng thái spool_corrupt (không gửi).
  3. Nếu inventory_hash, checks_hash và content_hash trùng bộ đã được xác nhận gần nhất: gửi SecurityReport kiểu HASH_ONLY dựng từ header (vài trăm byte). Nếu không: gửi body nguyên trạng.
  4. Ack có need_full = true: gửi lại body FULL củ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 ​

  1. Người có quyền security.hosts.manage tạ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ột Server). Access Hub chỉ cấp khi công ty có tính năng security_scanning và còn chỗ trong hạn mức scan_hosts.
  2. 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êng 0600 trong /var/lib/accesshub-scan-uploader/), gửi POST /scan/v1/enroll {"token", "public_key", "machine_id_sha256", "hostname", "os": {"id", "version_id"}, "uploader_version"}.
  3. Ingest gọi Access Hub POST /api/v1/security/matcher/scan-hosts/enroll để đổi mã lấy bản ghi scan_hosts (Access Hub kiểm mã, giấy phép, gắn Server theo mã hoặc theo machine_id_sha256 khớp máy đã có, hoặc tạo Server mới), nhận {host_id, company_id, server_id}, trả bộ gửi 201 {"host_id"}. Mã sai, hết hạn, đã dùng: 401 invalid_enrolment; hết giấy phép hoặc hạn mức: 403 licence_inactive.
  4. Mỗi request sau đó mang Authorization: AHS-Ed25519 host_id=<id>, ts=<unix ms>, sig=<base64>, chữ ký trên method \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ổ đó (401 bad_signature).
  5. 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: 403 licence_inactive.
  6. Xoay khóa: bộ gửi gọi POST /scan/v1/rotate ký 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)), PATH là đường dẫn URL không có query, body là đúng byte trên dây (đã nén nếu Content-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 PEM 0600, bộ gửi từ chối khóa nhóm hoặc người khác đọc được.
  • POST /scan/v1/rotate body {"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/config trả SecurityConfig protobuf với ETag là 16 byte đầu SHA-256 (hex) của bản mã hóa xác định; If-None-Match khớ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_window dướ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èm POST /scan/v1/enroll (hai trường địa chỉ mới, tùy chọn) và làm mới bằng PUT /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 UDP connect khô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): hostname rỗ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_ipv4 là IPv4 dạng chữ, không phải loopback, link-local, multicast, 0.0.0.0; primary_ipv6 là 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_version tố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 body scan-hosts/enroll như cũ (thêm primary_ipv4, primary_ipv6).
  • 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 theo report_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 theo host_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_sha256 là 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_id có thể null (bổ sung 2026-10-03): khi máy Scanner chưa gắn với Server nà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 theo host_id (khóa ổn định, finding_key không đổi khi gắn sau). Sự kiện gửi Access Hub mang host_id và server_id: null cho 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ấy server_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ẫn host_id, luôn trong phạm vi company_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 sau since. Bản ghi có company_id, server_id khô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}/rotate body {"public_key"}; 2xx là xong.
    • GET /api/v1/security/matcher/settings/{company_id}: {"data": {"security": {...}}} như mục 8; thiếu security nghĩ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).

proto
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ạnGiá trịVượt thì
Body sau nén, sau giải nén1 MiB, 8 MiB (như mọi endpoint)413
Số gói mỗi phần20.000413; scanner chia phần trước
Số phần mỗi báo cáo8413
Số CheckResult500413
evidenceTối đa 20 khóa, khóa ^[a-z][a-z0-9_]{0,31}$, giá trị tối đa 256 byte UTF-8Cắt giá trị, bỏ khóa thừa, cảnh báo
ListeningSocket1.024Cắt, cảnh báo
Chuỗi tên, phiên bản góiTối đa 256 byte, tên và phiên bản khác rỗng400 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_msKhông cũ hơn 7 ngày, không quá 5 phút ở tương lai422 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.

proto
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ườngRà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
previewRỗ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
pathTuyệt đối, tối đa 1.024 byte UTF-8 hợp lệ
Số lượngcontent 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ướcPhầ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 ​

HTTPcodeBộ gửi làm gì
202Ghi report_id đã xác nhận. need_full thì gửi FULL ngay
400bad_requestBỏ bộ báo cáo 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ờ (có thể máy đã bị thu hồi hoặc cần enroll lại)
403licence_inactiveNgừng gửi, giữ spool, trạng thái licence_inactive, thử lại sau 6 giờ
404Ingest cũ chưa có endpoint: tạm dừng 1 giờ, trạng thái ingest_unsupported
413too_largeKhông tự chia (scanner đã chia), bỏ bộ, trạng thái report_too_large
422unsupported_schema, stale_reportGiữ 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ử
429rate_limitedTheo Retry-After
5xx, lỗi mạngBackoff 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_count mới xử lý, phần lẻ quá 1 giờ thì bỏ.
  • Secmatch chỉ nhận báo cáo có scanned_at_ms mới hơn bản đang lưu của máy; báo cáo cũ hơn đến muộn trả accepted nhưng không đổi gì.
  • HASH_ONLY mà secmatch không có bản khớp inventory_hash của máy (mất dữ liệu, máy mới chuyển công ty): ingest trả ngay ack need_full = true nếu biết, nếu không matcher đánh dấu và ingest trả need_full ở lần sau (ingest giữ cờ theo host_id).
  • Sự kiện gửi Access Hub mang event_id = SHA-256(finding_key | transition_seq), seq tăng chặt theo máy. Access Hub khử trùng theo event_id và bỏ sự kiện có seq nhỏ hơn seq đã á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).

typePayload chính
finding.openedfinding_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.updatedfinding_key, các trường đổi (điểm, EPSS, KEV, fixed_version, fix_status, packages)
finding.resolvedfinding_key, resolved_reason (package_updated, package_removed, advisory_changed, check_passed, content_removed, file_deleted, out_of_scope, fingerprint_reset), resolved_at
check.statecheck_id, check_version, subject, status, evidence, observed_at (cho cả pass, error, unknown để hiển thị độ phủ)
server.scannedscanned_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}/summarySố máy có inventory, tuổi báo cáo cũ nhất, phân bố distro
DELETE /internal/v1/companies/{company_id}/dataXó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/statusPhiên bản, db_version, db_created_at, db_stale, độ sâu outbox, tiến độ so khớp lại
GET /internal/v1/stats/globalSố 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):

json
{"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):

json
{"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):

json
{"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ương HH:MM. end trước start là khung qua nửa đêm và thuộc ngày bắt đầu: days: ["mon"] với 22:00 đến 02:00 là từ 22:00 thứ Hai tới 02:00 thứ Ba. end bằng start là cả ngày. Đầu khung tính, cuối khung không tính.
  • timezone (tên IANA, ưu tiên) hoặc utc_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, days 0 là Chủ nhật). Khung sai định dạng thì ingest bỏ khung (ghi log) và bộ đọc cũng bỏ khung sai (đếm config_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_window và chờ; vào khung thì quét. Yêu cầu quét sớm (scan_requested_at mới hơn lần quét đầy đủ cuối) vượt khung đúng một lần. accesshub-scanner scan --force củ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 qua config.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.v1 và accesshub.secmatch.v1 của repo này, kiểm bằng buf breaking trong CI của repo này. Endpoint /scan/v1 chỉ thay đổi bổ sung.
  • schema_version củ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ền schema_version chấp nhận để trả 422 đồng bộ.
  • API /internal/v1 của secmatch và /api/v1/security/matcher của Access Hub chỉ thay đổi bổ sung; đổi ngữ nghĩa thì tăng v.
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.