tencent-blade-fetch-processor — Design
Status: Implemented — Wayback Machine ingestion path
Source: Wayback Machine snapshots ofblade.tencent.com/en/advisories/<slug>/(the live Tencent Blade portal is geo-restricted to mainland China and unreachable fromap-southeast-2) Type:fetch(HTTP fetch againstweb.archive.org)
Processor Name:tencent-blade-fetch-processor
AWS Schedule Name:go-tencent-blade-fetch-processor
Source Namespace:tencent-blade
1. Overview
- Purpose: Ingest Tencent Blade Team advisories via the Wayback
Machine. The original portal at
blade.tencent.comis geo-blocked (verified live: connections from ap-southeast-2 to the main domain hang). The Wayback Machine has indexed the English advisory pages and is publicly reachable from any region. - Owner: Tencent Blade Team (elite security research team within Tencent)
- Licence: Publicly disclosed vulnerabilities; archival use covered by Wayback’s terms.
- Schedule cadence: Runs weekly on Sundays at 04:00 UTC (
cron(0 4 ? * SUN *)). - Rationale: Low volume (~5 historical advisories indexed; ~1–3 new per month upper bound based on the team’s publication rate); daily check picks up new Wayback-indexed snapshots within a day of crawl.
- ECS resources:
cpu_units=256,memory_mb=512,expected_duration_minutes=15 - Reads: Wayback CDX API + archived HTML;
BulkDataDumpTracker(freshness);LoadProcessedHashes(resume) - Writes:
CVEMetadata,CVEDescription,CVEMetadataReferences,CVEAlias(same-cveIdcross-source edges only),BulkDataDumpTracker; S3 archive + quarantine of the archived HTML. Plus the shared pipeline’s derivedcontainerType="vulnetix"cvssV4_0CVEMetricrow. - Never written in practice: an upstream
CVEMetricrow (the parser populates no CVSS vector — see §5.4),CVEMetadata.affectedVendor/affectedProduct(parsed fields exist on the struct but nothing fills them),CVEAffected/CVEAffectedVersion, and anyinternal/aienrichoutput (no enricher is constructed). - Soft deadline:
softDeadlineDurationdefaults to 15 minutes unconditionally (main.go:80-84), so unsettingEXPECTED_DURATION_MINUTESin the backfill recipe does not remove the cap.
2. Source Contract — Wayback architecture
| Attribute | Value |
|---|---|
| List endpoint | http://web.archive.org/cdx/search/cdx?url=blade.tencent.com/en/advisories/*&output=json&filter=mimetype:text/html&filter=statuscode:200&collapse=urlkey |
| Detail endpoint | https://web.archive.org/web/2/{originalURL} (the /2/ magic prefix redirects to the most-recent snapshot) |
| List schema | JSON: [[urlkey, timestamp, original, mimetype, statuscode, digest, length], ...rows]; first row is the header |
| Filter | Drop the index URL (/en/advisories/ exactly); keep only leaf advisory paths matching ^https?://blade\.tencent\.com/en/advisories/<slug>/?$ |
| Identifier | CVE-YYYY-NNNNN extracted via regex from the archived advisory body. Primary CVE = first match. Sibling CVEs are NOT written as aliases — internal/tencentblade/parser.go:214-219 turns them into cve.mitre.org CVEMetadataReferences rows labelled alias-<CVE> instead, and MapAdvisory leaves CVESourceData.Aliases nil, so db.InsertAliases only ever writes the same-cveId cross-source edges. Multi-CVE Blade advisories are therefore invisible to alias-graph traversal. |
| Volume | 5 distinct English advisories indexed (LoRaDawn, QualPwn, Magellan, Magellan v2, V-Ghost) — verified live. The full set Tencent has published may be larger, but this is the bound on what Wayback exposes today. |
| Cadence | Bounded by Wayback crawl frequency (typically days, not hours, behind real-time) |
| Freshness signal | Use BulkDataDumpTracker + sourceFileHash (sha256 of the CDX response). New advisories appear when Wayback indexes them. |
| Anti-bot / auth | None on Wayback; CDX is open. Detail fetches courteously rate-limited to 1 req/sec. |
| Cross-reference (defensive) | The processor records the canonical (un-archived) blade.tencent.com URL as the advisory reference, even though we cannot fetch it directly. |
Sample CDX row (live, May 2026):
["com,tencent,blade)/en/advisories/loradawn",
"20220314035841",
"https://blade.tencent.com/en/advisories/loradawn/",
"text/html", "200", "ZRJA7NG6TUVLN5XXJDX5E7VD2KYUBDGS", "4137"]
→ DetailURL becomes https://web.archive.org/web/20220314035841/https://blade.tencent.com/en/advisories/loradawn/
3. Architecture
cron(0 6 * * ? *)] --> ECS[ECS Fargate
go-tencent-blade-fetch-processor] ECS --> Tracker{BulkDataDumpTracker
fresh?} Tracker -- yes --> Skip[no-op
notify NoWork] Tracker -- no --> List[GET /en/advisories/cve-list/] List --> ParseList[tencentblade.ParseListPage
extract []ListEntry] ParseList --> Loop[for each entry] Loop --> Known{already processed?
sourceFileHash check} Known -- yes --> SkipAdv[skip] Known -- no --> FetchAdv[GET detail page
1 req/sec rate limit] FetchAdv --> ParseAdv[tencentblade.ParseDetailPage
extract *Advisory] ParseAdv --> Map[tencentblade.MapAdvisory
*osv.CVESourceData] Map --> Tx[db.WithTx] Tx --> Pipeline[processor.StoreCVESourceData] Pipeline --> CVEMetadata Pipeline --> CVEAlias[(db.InsertAliases
same-cveId cross-source)] Pipeline --> CVEDescription & CVEReference & CVEMetric Tx --> Commit[COMMIT] Commit --> Archive[s3client.Uploader.Archive
detail page HTML] ParseAdv -- error --> Quarantine[s3client.Uploader.Quarantine] Loop --> TrackerW[(BulkDataDumpTracker
upsert listHash)] TrackerW --> Notify[notify Completed/Errored]
4. Data Flow
5. Source → DB Field Mapping
5.1 CVEMetadata
| Column | Source / Derivation | Status |
|---|---|---|
cveId | Parsed CVE ID from list or detail page | Confirmed |
source | "tencent-blade" | Confirmed |
dataVersion | "1.0" | Confirmed |
state | "PUBLISHED" | Confirmed |
datePublished | “On Month D, YYYY” in the body, else the first ISO date in the body, else 0 | Confirmed — no fallback to the Wayback snapshot timestamp or to the CVE id’s year, so 4 of the 5 stored rows carry datePublished = 0 |
title | Archived <title>, Wayback prefix stripped; falls back to a slug-derived title | Confirmed |
sourceAdvisoryRef | Wayback snapshot URL (the web.archive.org/web/… form, not the canonical blade.tencent.com URL) | Confirmed |
affectedVendor | — | Never populated. Advisory.AffectedVendor exists on the struct and is copied by the mapper, but no parser path assigns it. |
affectedProduct | — | Never populated (same reason). |
lastFetchedAt | time.Now().UnixMilli() | Confirmed |
rawDataJSON | JSON envelope with all scraped fields | Confirmed |
sourceFileHash | sha256(detail page HTML) | Confirmed |
5.2 CVEDescription
| Column | Value |
|---|---|
cveId | CVE ID |
source | "tencent-blade" |
containerType | "cna" |
lang | "en" |
value | Full description from detail page |
5.3 CVEMetadataReferences
Only two kinds of URL are emitted — the parser does not scan the archived
page for arbitrary links. ParseDetailPage builds the list from (a) the
canonical un-archived blade.tencent.com advisory URL and (b) one
cve.mitre.org/cgi-bin/cvename.cgi?name=<CVE> entry per sibling CVE found in
the body. MapAdvisory then classifies each by URL shape:
| URL Pattern | Type | ReferenceSource |
|---|---|---|
blade.tencent.com/... | advisory | Tencent Blade |
cve.mitre.org/... | advisory | MITRE |
nvd.nist.gov/... | advisory | NVD |
| GitHub commit/PR/issue | patch / issue | GitHub |
| Vendor security bulletin | advisory | Vendor name |
5.4 CVEMetric
MapAdvisory emits a cna metric row when Advisory.CVSSVector is non-empty:
| ContainerType | MetricType | VectorString | BaseScore | BaseSeverity |
|---|---|---|---|---|
cna | cvssV3_1 or cvssV4_0 | Parsed vector | Parsed score | Derived from score |
In practice this branch is dead: ParseDetailPage never assigns
CVSSVector / CVSSScore / CVSSVersion / Severity, so no upstream metric
row has ever been written for source='tencent-blade'. The only metric these
records carry is the shared pipeline’s description-derived
containerType="vulnetix" cvssV4_0 row. Archived Blade advisories do quote a
CVSS vector in prose, so this is an extraction gap rather than an upstream
limitation.
5.5 CVEAlias
Always call db.InsertAliases(ctx, tx, cveID, source, nil, logger) — even with nil aliases, per AGENTS.md alias contract. This ensures same-cveId cross-source edges are backfilled. No bundle suppression needed (1 CVE per advisory).
6. Identifier Policy
| Decision | Value | Rationale |
|---|---|---|
CVEMetadata.cveId | CVE-YYYY-NNNNN | Native CVE IDs; maximizes downstream hit-rate |
CVEAlias rows | Same-cveId cross-source only | No source-prefixed IDs to alias |
| Bundle suppression | Not applicable | Each advisory describes exactly 1 CVE |
| Minted IDs | None | Source emits native CVEs |
7. CRIT / VEX
No CRIT staging needed. Tencent Blade Team is a security research team, not a cloud provider. Their advisories do not attribute vulnerabilities to (Provider, Service, ResourceType) triples. No extended dictionaries needed.
8. S3 / Source-File Archive Layout
- Bucket:
S3_BUCKET_NAME(from env) - Archive key prefix:
tencent-blade/files/{sha256}/{filename} - Quarantine key prefix:
failed-feeds/tencent-blade-fetch-processor/{YYYY-MM-DD}/{reason}/{filename} - Payload schema: Raw detail page HTML (preserved for round-trip debugging)
- Quarantine reasons:
parse-error— detail page HTML could not be parsedstore-error—processor.StoreCVESourceDatareturned an error
9. Error Handling & Slack
- Per-record failures:
notifier.RecordError(msg); batch continues - Fatal failures (list page fetch, DB connection, tracker update):
notifier.Errored(processor, stats, err)+os.Exit(1) - NoWork:
notifier.NoWork(processor, "data is fresh")when tracker is within frequency - Retry profile: 3 attempts with exponential backoff (2s, 4s) for list page; per-detail-page fetch has 1 attempt (low volume, re-run next day is fine)
- Slack stats dict:
{"fetched": N, "stored": N, "skipped": N, "failed": N}
10. Performance
- Concurrency: Sequential (low volume, no need for workers)
- Rate limit: 1 req/sec between detail page requests
- Per-request timeout: 15 seconds
- Soft deadline:
EXPECTED_DURATION_MINUTES - 10minutes (default 5 min if env unset) - Resume strategy:
db.LoadProcessedHashes(hash-based skip set) - Pool sizing:
db.NewPooldefaults (read=5, write=5)
11. Backfill
Not warranted. The source is small enough (~50–150 total advisories) that --force --limit=0 on the same binary covers full historical reprocessing in a single run. No separate cmd/tencent-blade-fetch-backfill/ needed.
The justfile recipe go-tencent-blade-fetch-backfill runs the same binary with EXPECTED_DURATION_MINUTES unset so backfill runs to completion.
12. Implementation Tasks
Phase 1: Reconnaissance & Discovery (BLOCKED)
| # | Task | Owner | Status |
|---|---|---|---|
| 1.1 | Fetch and save blade-list.html from unrestricted network | TBD | ⬜ |
| 1.2 | Fetch and save 2–3 blade-detail-*.html pages | TBD | ⬜ |
| 1.3 | Document list page structure: pagination, fields, URL patterns | TBD | ⬜ |
| 1.4 | Document detail page structure: CSS selectors, embedded JSON, meta tags | TBD | ⬜ |
| 1.5 | Test rate limits: burst requests, observe 429/retry-after behavior | TBD | ⬜ |
| 1.6 | Implement ParseListPage and ParseDetailPage with confirmed selectors | TBD | ⬜ |
Phase 2: Go Processor Implementation (DONE — stubs in place)
| # | Task | File | Status |
|---|---|---|---|
| 2.1 | Create cmd/tencent-blade-fetch-processor/main.go | scripts/go-processors/cmd/tencent-blade-fetch-processor/main.go | ✅ |
| 2.2 | Create internal/tencentblade/types.go | scripts/go-processors/internal/tencentblade/types.go | ✅ |
| 2.3 | Create internal/tencentblade/parser.go | scripts/go-processors/internal/tencentblade/parser.go | ✅ (stubs) |
| 2.4 | Create internal/tencentblade/mapper.go | scripts/go-processors/internal/tencentblade/mapper.go | ✅ |
| 2.5 | Implement list page fetch + parse | main.go + parser.go | ⬜ (blocked on recon) |
| 2.6 | Implement detail page fetch + parse | main.go + parser.go | ⬜ (blocked on recon) |
| 2.7 | Implement DB storage via processor.StoreCVESourceData | main.go | ✅ |
| 2.8 | Implement S3 archive/quarantine hooks | main.go | ✅ |
| 2.9 | Implement idempotency / resume logic | main.go | ✅ |
| 2.10 | Implement freshness gate | main.go | ✅ |
| 2.11 | Add structured logging + notifier integration | main.go | ✅ |
| 2.12 | Add --force, --limit, --all CLI flags | main.go | ✅ |
Phase 3: Infrastructure & Configuration (DONE)
| # | Task | File | Status |
|---|---|---|---|
| 3.1 | Add Containerfile target | Containerfile.go-processors | ✅ |
| 3.2 | Add task-manager.toml entry | scripts/task-manager.toml | ✅ |
| 3.3 | Add terraform locals + module | terraform/go-schedules.tf | ✅ |
| 3.4 | Add justfile recipe | justfile | ✅ |
| 3.5 | Add ECR push hook target | .claude/hooks/post-push-ecr.sh | ✅ |
| 3.6 | Update targets.go | scripts/task-dashboard/cmd/ecr-build/targets.go | ✅ |
Phase 4: Testing & Validation (after recon)
| # | Task | Command / Method |
|---|---|---|
| 4.1 | Local build test | cd scripts/go-processors && go build ./cmd/tencent-blade-fetch-processor |
| 4.2 | Local run test (with DB) | just go-tencent-blade-fetch-backfill |
| 4.3 | Verify DB records | SELECT * FROM "CVEMetadata" WHERE source = 'tencent-blade' LIMIT 10; |
| 4.4 | Verify descriptions | SELECT * FROM "CVEDescription" WHERE source = 'tencent-blade'; |
| 4.5 | Verify references | SELECT * FROM "CVEReference" WHERE source = 'tencent-blade'; |
| 4.6 | Verify aliases | SELECT * FROM "CVEAlias" WHERE "discoveredFrom" = 'tencent-blade'; |
| 4.7 | Idempotency test | Run twice; second run should report skipped = count of first run |
| 4.8 | Container build test | podman build -f Containerfile.go-processors --target tencent-blade-fetch-processor -t test . |
| 4.9 | Terraform validate | cd terraform && terraform fmt && terraform validate |
13. File Checklist
New Files
-
scripts/go-processors/cmd/tencent-blade-fetch-processor/main.go -
scripts/go-processors/internal/tencentblade/types.go -
scripts/go-processors/internal/tencentblade/parser.go -
scripts/go-processors/internal/tencentblade/mapper.go -
scripts/go-processors/tencent-blade-fetch-processor.design.md(this file)
Modified Files
-
Containerfile.go-processors— addtencent-blade-fetch-processortarget -
scripts/task-manager.toml— add[tasks.tencent-blade-fetch-processor] -
terraform/go-schedules.tf— add local + module -
justfile— addgo-tencent-blade-fetch-backfillrecipe -
.claude/hooks/post-push-ecr.sh— add toTARGETS -
scripts/task-dashboard/cmd/ecr-build/targets.go— add tosimpleTargets
14. Naming Convention Compliance
Per AGENTS.md Processor Naming Convention:
| System | Value | ✅ |
|---|---|---|
cmd/ directory | tencent-blade-fetch-processor | ✅ |
| Containerfile target | AS tencent-blade-fetch-processor | ✅ |
| Go binary output | /tencent-blade-fetch-processor | ✅ |
| ECR image tag | go-tencent-blade-fetch-processor-${var.tag} | ✅ |
| ECS task definition family | go-tencent-blade-fetch-processor | ✅ |
| ECS container name | tencent-blade-fetch-processor | ✅ |
| EventBridge schedule name | go-tencent-blade-fetch-processor | ✅ |
| CloudWatch log group | /ecs/vdb-scheduler/go-tencent-blade-fetch-processor | ✅ |
| CloudWatch stream prefix | tencent-blade-fetch-processor | ✅ |
| task-manager.toml key | [tasks.tencent-blade-fetch-processor] | ✅ |
task-manager.toml aws_schedule | go-tencent-blade-fetch-processor | ✅ |
| terraform module name | tencent_blade_fetch_processor | ✅ |
terraform task_name | tencent-blade-fetch-processor | ✅ |
| justfile recipe | go-tencent-blade-fetch-backfill | ✅ |
| post-push-ecr.sh TARGET | tencent-blade-fetch-processor | ✅ |
15. Risk Register
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Site uses heavy JS framework requiring headless browser | Medium | High | Recon will determine. If needed, evaluate go-rod (like cert-il-fetch-processor). Low volume makes headless feasible. |
| Geo-blocking / WAF prevents ECS fetch | Medium | High | Test from ap-southeast-2 after first deploy. If blocked, evaluate proxy or alternative source. Browser headers + conservative rate limit are first-line defense. |
| Site structure changes after implementation | Low | Medium | sourceFileHash change detection + S3 quarantine on parse failures. Parse errors are logged and alerted via Slack. |
| Low advisory volume makes daily schedule wasteful | Low | Low | Tracker freshness gate ensures no-op exits fast (~1s). Adjust cron to weekly if needed. |
| Detail pages require authentication | Low | High | Recon will catch this. If true, processor may be infeasible without API key. |
| Rate limiting is aggressive | Medium | Medium | Conservative delays (1 req/sec), exponential backoff on 429, limit batch size. Low volume means even aggressive limits are tolerable. |
16. Appendix: Reference Processors
Similar fetch processors to study during implementation:
| Processor | Similarity | Key File |
|---|---|---|
pwno-fetch-processor | Single-page scrape (no pagination), HTML parsing, hash-based resume | cmd/pwno-fetch-processor/main.go |
cert-be-fetch-processor | RSS feed → per-page HTML scrape, S3 archive, processor.StoreCVESourceData | cmd/cert-be-fetch-processor/main.go |
snyk-fetch-processor | Listing pagination → detail page scrape, resume logic | cmd/snyk-fetch-processor/main.go |
17. Open Questions
What is the exact detail page URL pattern?
Hypothesis:https://blade.tencent.com/en/advisories/detail/{cve-id}/or similar. Needs confirmation.Does the list page show all advisories or is it paginated?
If paginated, what is the pagination mechanism (?page=N,?offset=N, cursor-based)?Is there embedded structured data (JSON-LD,
application/ld+json)?
Many modern advisory sites include Schema.orgVulnerabilitymarkup.Does the site publish CVSS v4.0 vectors or only v3.1?
AffectsCVEMetricrow construction.Are there non-CVE advisories (e.g., Blade-specific IDs)?
If yes, primarycveIdmay need to be a Blade ID with CVE as alias.Is there a Chinese version with more complete data?
URL:https://blade.tencent.com/zh/advisories/cve-list/— may have more advisories or richer descriptions.
S3 Persistence
- Archive path:
tencent-blade/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/tencent-blade-fetch-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
parse-error,store-error
Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).
See the S3 Persistence Contract for the full reason taxonomy.