tencent-blade-fetch-processor — Design

Status: Implemented — Wayback Machine ingestion path
Source: Wayback Machine snapshots of blade.tencent.com/en/advisories/<slug>/ (the live Tencent Blade portal is geo-restricted to mainland China and unreachable from ap-southeast-2) Type: fetch (HTTP fetch against web.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.com is 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-cveId cross-source edges only), BulkDataDumpTracker; S3 archive + quarantine of the archived HTML. Plus the shared pipeline’s derived containerType="vulnetix" cvssV4_0 CVEMetric row.
  • Never written in practice: an upstream CVEMetric row (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 any internal/aienrich output (no enricher is constructed).
  • Soft deadline: softDeadlineDuration defaults to 15 minutes unconditionally (main.go:80-84), so unsetting EXPECTED_DURATION_MINUTES in the backfill recipe does not remove the cap.

2. Source Contract — Wayback architecture

AttributeValue
List endpointhttp://web.archive.org/cdx/search/cdx?url=blade.tencent.com/en/advisories/*&output=json&filter=mimetype:text/html&filter=statuscode:200&collapse=urlkey
Detail endpointhttps://web.archive.org/web/2/{originalURL} (the /2/ magic prefix redirects to the most-recent snapshot)
List schemaJSON: [[urlkey, timestamp, original, mimetype, statuscode, digest, length], ...rows]; first row is the header
FilterDrop the index URL (/en/advisories/ exactly); keep only leaf advisory paths matching ^https?://blade\.tencent\.com/en/advisories/<slug>/?$
IdentifierCVE-YYYY-NNNNN extracted via regex from the archived advisory body. Primary CVE = first match. Sibling CVEs are NOT written as aliasesinternal/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.
Volume5 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.
CadenceBounded by Wayback crawl frequency (typically days, not hours, behind real-time)
Freshness signalUse BulkDataDumpTracker + sourceFileHash (sha256 of the CDX response). New advisories appear when Wayback indexes them.
Anti-bot / authNone 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

graph LR EB[EventBridge
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

sequenceDiagram participant Cron as EventBridge (daily 06:00) participant Proc as tencent-blade-fetch-processor participant Site as blade.tencent.com participant DB as PostgreSQL participant S3 as S3 Bucket Cron->>Proc: ECS RunTask Proc->>DB: GetTracker(source=tencent-blade) DB-->>Proc: lastProcessedAt, frequency alt fresh & not --force Proc-->>Cron: NoWork (exit 0) end Proc->>Site: GET /en/advisories/cve-list/ Site-->>Proc: HTML list page Proc->>Proc: tencentblade.ParseListPage → []ListEntry Note over Proc: Each entry: cveID, title, date, severity, detailURL Proc->>DB: LoadProcessedHashes(source=tencent-blade) DB-->>Proc: map[cveID]sourceFileHash loop for each entry (rate-limited 1 req/sec) Proc->>Proc: check sourceFileHash in seen set alt already known & not --force Proc->>Proc: skipped++ else Proc->>Site: GET detailURL Site-->>Proc: HTML detail page Proc->>Proc: tencentblade.ParseDetailPage → *Advisory Proc->>Proc: tencentblade.MapAdvisory → *osv.CVESourceData Proc->>DB: BEGIN Proc->>DB: processor.StoreCVESourceData Proc->>DB: COMMIT Proc->>S3: Archive(detail page HTML) end end Proc->>DB: UpsertTracker(source=tencent-blade, listHash, stored) Proc-->>Cron: Completed{stored, skipped, failed}

5. Source → DB Field Mapping

5.1 CVEMetadata

ColumnSource / DerivationStatus
cveIdParsed CVE ID from list or detail pageConfirmed
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 0Confirmed — no fallback to the Wayback snapshot timestamp or to the CVE id’s year, so 4 of the 5 stored rows carry datePublished = 0
titleArchived <title>, Wayback prefix stripped; falls back to a slug-derived titleConfirmed
sourceAdvisoryRefWayback snapshot URL (the web.archive.org/web/… form, not the canonical blade.tencent.com URL)Confirmed
affectedVendorNever populated. Advisory.AffectedVendor exists on the struct and is copied by the mapper, but no parser path assigns it.
affectedProductNever populated (same reason).
lastFetchedAttime.Now().UnixMilli()Confirmed
rawDataJSONJSON envelope with all scraped fieldsConfirmed
sourceFileHashsha256(detail page HTML)Confirmed

5.2 CVEDescription

ColumnValue
cveIdCVE ID
source"tencent-blade"
containerType"cna"
lang"en"
valueFull 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 PatternTypeReferenceSource
blade.tencent.com/...advisoryTencent Blade
cve.mitre.org/...advisoryMITRE
nvd.nist.gov/...advisoryNVD
GitHub commit/PR/issuepatch / issueGitHub
Vendor security bulletinadvisoryVendor name

5.4 CVEMetric

MapAdvisory emits a cna metric row when Advisory.CVSSVector is non-empty:

ContainerTypeMetricTypeVectorStringBaseScoreBaseSeverity
cnacvssV3_1 or cvssV4_0Parsed vectorParsed scoreDerived 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

DecisionValueRationale
CVEMetadata.cveIdCVE-YYYY-NNNNNNative CVE IDs; maximizes downstream hit-rate
CVEAlias rowsSame-cveId cross-source onlyNo source-prefixed IDs to alias
Bundle suppressionNot applicableEach advisory describes exactly 1 CVE
Minted IDsNoneSource 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 parsed
    • store-errorprocessor.StoreCVESourceData returned 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 - 10 minutes (default 5 min if env unset)
  • Resume strategy: db.LoadProcessedHashes (hash-based skip set)
  • Pool sizing: db.NewPool defaults (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)

#TaskOwnerStatus
1.1Fetch and save blade-list.html from unrestricted networkTBD
1.2Fetch and save 2–3 blade-detail-*.html pagesTBD
1.3Document list page structure: pagination, fields, URL patternsTBD
1.4Document detail page structure: CSS selectors, embedded JSON, meta tagsTBD
1.5Test rate limits: burst requests, observe 429/retry-after behaviorTBD
1.6Implement ParseListPage and ParseDetailPage with confirmed selectorsTBD

Phase 2: Go Processor Implementation (DONE — stubs in place)

#TaskFileStatus
2.1Create cmd/tencent-blade-fetch-processor/main.goscripts/go-processors/cmd/tencent-blade-fetch-processor/main.go
2.2Create internal/tencentblade/types.goscripts/go-processors/internal/tencentblade/types.go
2.3Create internal/tencentblade/parser.goscripts/go-processors/internal/tencentblade/parser.go✅ (stubs)
2.4Create internal/tencentblade/mapper.goscripts/go-processors/internal/tencentblade/mapper.go
2.5Implement list page fetch + parsemain.go + parser.go⬜ (blocked on recon)
2.6Implement detail page fetch + parsemain.go + parser.go⬜ (blocked on recon)
2.7Implement DB storage via processor.StoreCVESourceDatamain.go
2.8Implement S3 archive/quarantine hooksmain.go
2.9Implement idempotency / resume logicmain.go
2.10Implement freshness gatemain.go
2.11Add structured logging + notifier integrationmain.go
2.12Add --force, --limit, --all CLI flagsmain.go

Phase 3: Infrastructure & Configuration (DONE)

#TaskFileStatus
3.1Add Containerfile targetContainerfile.go-processors
3.2Add task-manager.toml entryscripts/task-manager.toml
3.3Add terraform locals + moduleterraform/go-schedules.tf
3.4Add justfile recipejustfile
3.5Add ECR push hook target.claude/hooks/post-push-ecr.sh
3.6Update targets.goscripts/task-dashboard/cmd/ecr-build/targets.go

Phase 4: Testing & Validation (after recon)

#TaskCommand / Method
4.1Local build testcd scripts/go-processors && go build ./cmd/tencent-blade-fetch-processor
4.2Local run test (with DB)just go-tencent-blade-fetch-backfill
4.3Verify DB recordsSELECT * FROM "CVEMetadata" WHERE source = 'tencent-blade' LIMIT 10;
4.4Verify descriptionsSELECT * FROM "CVEDescription" WHERE source = 'tencent-blade';
4.5Verify referencesSELECT * FROM "CVEReference" WHERE source = 'tencent-blade';
4.6Verify aliasesSELECT * FROM "CVEAlias" WHERE "discoveredFrom" = 'tencent-blade';
4.7Idempotency testRun twice; second run should report skipped = count of first run
4.8Container build testpodman build -f Containerfile.go-processors --target tencent-blade-fetch-processor -t test .
4.9Terraform validatecd 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 — add tencent-blade-fetch-processor target
  • scripts/task-manager.toml — add [tasks.tencent-blade-fetch-processor]
  • terraform/go-schedules.tf — add local + module
  • justfile — add go-tencent-blade-fetch-backfill recipe
  • .claude/hooks/post-push-ecr.sh — add to TARGETS
  • scripts/task-dashboard/cmd/ecr-build/targets.go — add to simpleTargets

14. Naming Convention Compliance

Per AGENTS.md Processor Naming Convention:

SystemValue
cmd/ directorytencent-blade-fetch-processor
Containerfile targetAS tencent-blade-fetch-processor
Go binary output/tencent-blade-fetch-processor
ECR image taggo-tencent-blade-fetch-processor-${var.tag}
ECS task definition familygo-tencent-blade-fetch-processor
ECS container nametencent-blade-fetch-processor
EventBridge schedule namego-tencent-blade-fetch-processor
CloudWatch log group/ecs/vdb-scheduler/go-tencent-blade-fetch-processor
CloudWatch stream prefixtencent-blade-fetch-processor
task-manager.toml key[tasks.tencent-blade-fetch-processor]
task-manager.toml aws_schedulego-tencent-blade-fetch-processor
terraform module nametencent_blade_fetch_processor
terraform task_nametencent-blade-fetch-processor
justfile recipego-tencent-blade-fetch-backfill
post-push-ecr.sh TARGETtencent-blade-fetch-processor

15. Risk Register

RiskLikelihoodImpactMitigation
Site uses heavy JS framework requiring headless browserMediumHighRecon will determine. If needed, evaluate go-rod (like cert-il-fetch-processor). Low volume makes headless feasible.
Geo-blocking / WAF prevents ECS fetchMediumHighTest 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 implementationLowMediumsourceFileHash change detection + S3 quarantine on parse failures. Parse errors are logged and alerted via Slack.
Low advisory volume makes daily schedule wastefulLowLowTracker freshness gate ensures no-op exits fast (~1s). Adjust cron to weekly if needed.
Detail pages require authenticationLowHighRecon will catch this. If true, processor may be infeasible without API key.
Rate limiting is aggressiveMediumMediumConservative 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:

ProcessorSimilarityKey File
pwno-fetch-processorSingle-page scrape (no pagination), HTML parsing, hash-based resumecmd/pwno-fetch-processor/main.go
cert-be-fetch-processorRSS feed → per-page HTML scrape, S3 archive, processor.StoreCVESourceDatacmd/cert-be-fetch-processor/main.go
snyk-fetch-processorListing pagination → detail page scrape, resume logiccmd/snyk-fetch-processor/main.go

17. Open Questions

  1. What is the exact detail page URL pattern?
    Hypothesis: https://blade.tencent.com/en/advisories/detail/{cve-id}/ or similar. Needs confirmation.

  2. Does the list page show all advisories or is it paginated?
    If paginated, what is the pagination mechanism (?page=N, ?offset=N, cursor-based)?

  3. Is there embedded structured data (JSON-LD, application/ld+json)?
    Many modern advisory sites include Schema.org Vulnerability markup.

  4. Does the site publish CVSS v4.0 vectors or only v3.1?
    Affects CVEMetric row construction.

  5. Are there non-CVE advisories (e.g., Blade-specific IDs)?
    If yes, primary cveId may need to be a Blade ID with CVE as alias.

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

flowchart LR SRC[Source feed] --> PROC[tencent-blade-fetch-processor] PROC -->|success| ARCHIVE[("S3: tencent-blade/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/tencent-blade-fetch-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.