Skip to content

feat(data-warehouse): implement qualys_vmdr import source - #71163

Merged
talyn-app[bot] merged 6 commits into
masterfrom
posthog-code/implement-qualys-vmdr-source
Jul 16, 2026
Merged

feat(data-warehouse): implement qualys_vmdr import source#71163
talyn-app[bot] merged 6 commits into
masterfrom
posthog-code/implement-qualys-vmdr-source

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

Problem

Qualys VMDR is a widely deployed enterprise vulnerability management platform. Data teams want its host-detection history in the warehouse to track remediation velocity and join vulnerability data with the rest of their analytics. The source was scaffolded (hidden, empty fields) in the batch scaffold PR; this fills in the implementation.

Why: completes the scaffolded qualys_vmdr connector so users can sync Qualys VMDR vulnerability data into the PostHog Data warehouse.

Changes

Implements the source end-to-end on the ResumableSource base, following the source.py / settings.py / qualys_vmdr.py architecture split:

  • Endpoints (settings.py): hosts (asset inventory), host_list_detection (per-host vulnerability detections, the core VMDR feed, flattened to one row per detection with host_*-prefixed host fields), scans (VM scan history), and knowledge_base (QID definitions, off by default since it needs a subscription add-on). All four use genuine server-side timestamp filters for incremental sync (vm_scan_since, detection_updated_since, launched_after_datetime, last_modified_after).
  • Transport (qualys_vmdr.py): the classic FO API returns XML, so this is a bespoke transport on make_tracked_session() rather than the generic JSON REST client. Handles the mandatory X-Requested-With header, HTTP Basic auth against the user-configured regional API server, keep-alive whitespace before the XML declaration, truncation-URL pagination (the WARNING block's next-batch URL, re-rooted onto the configured server so credentials never follow a response-supplied host), and 409 rate limits honoring X-RateLimit-ToWait-Sec.
  • Resumable state: the next-batch URL is saved after each yielded batch, so Temporal resumes mid-pagination instead of restarting. sort_mode=\"desc\" because batches are ordered by record id, not by the incremental field, so the watermark only persists at successful job end.
  • Nested XML structures (tags, CVE lists) are JSON-encoded strings rather than structs, since element repetition varies per record and would otherwise produce conflicting Arrow types within a batch.
  • api_server is declared as a connection_host_fields entry so retargeting it forces credential re-entry.
  • Canonical table/column descriptions, SOURCES.md moved to Implemented, generated QualysVmdrSourceConfig, released visible with releaseStatus=ReleaseStatus.ALPHA (removed unreleasedSource=True).

Note

I could not verify endpoint behavior against a live Qualys subscription (no credentials available), so parameter names and truncation semantics follow the current Qualys API v2 user guide plus existing Airbyte/Fivetran connector behavior, and the riskier assumptions are marked with comments. In particular launched_after_datetime on the scan list and the absence of truncation on scans/knowledge_base are doc-based. The source ships as alpha accordingly.

How did you test this code?

Ran the new targeted test suites (38 tests) plus the registry-wide source tests (sources/tests/, 1727 passed; the 2 test_stripe_config failures pre-exist on the base branch), ruff check/ruff format, and hogli ci:preflight --fix. No live-API testing was performed.

  • tests/test_qualys_vmdr.py (transport): catches regressions in truncation pagination (dropping the WARNING URL would silently truncate syncs), resume-state ordering (saving before yield would skip a batch on crash), incremental filter construction (dropping vm_scan_since would silently full-refresh), detection flattening (losing unique_vuln_id or the host_ prefix would corrupt the primary key), next-URL re-rooting (following a response-supplied host would leak credentials), rate-limit wait parsing, and credential-validation status mapping (401 vs 403/409).
  • tests/test_qualys_vmdr_source.py (source class): catches re-hiding the source (unreleasedSource), losing the api_server credential-retarget guard, wrong primary/partition keys or sort mode per endpoint, and passing a stale watermark when incremental is disabled.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

No posthog.com checkout was available in this environment, so the user-facing doc below needs to land in the posthog.com repo at contents/docs/cdp/sources/qualys-vmdr.md (matching the source's docsUrl); audit_source_docs should be run once it does. The source sets lists_tables_without_credentials = True so <SourceTables /> renders the table catalog.

Doc content for posthog.com
---
title: Linking Qualys VMDR as a source
sidebar: Docs
showTitle: true
availability: { free: full, selfServe: full, enterprise: full }
sourceId: QualysVmdr
beta: true
---

import SourceSetupIntro from "../_snippets/source-setup-intro.mdx"
import SyncModes from "../_snippets/sync-modes.mdx"
import TroubleshootingLink from "../_snippets/dw-troubleshooting-link.mdx"
import AlphaRelease from "../_snippets/alpha-release.mdx"

<AlphaRelease />

The Qualys VMDR connector syncs your vulnerability management data into PostHog: host asset inventory, per-host vulnerability detections, scan history, and the Qualys KnowledgeBase. Use it to track remediation velocity and join detection data with the rest of your warehouse.

## Prerequisites

- A Qualys subscription with the VM/VMDR module enabled.
- A Qualys user with API access (a Manager role, or a role granted API access).
- Your account's regional API server URL, shown under **Help > About** in the Qualys UI (for example \`qualysapi.qualys.com\`, \`qualysapi.qg2.apps.qualys.com\`, or \`qualysapi.qualys.eu\`).
- For the \`knowledge_base\` table only: the KnowledgeBase download option enabled on your subscription (contact Qualys support if it isn't).

## Adding a data source

<SourceSetupIntro />

Enter your regional API server URL and the username and password of the API-enabled Qualys user. PostHog authenticates with HTTP Basic auth over TLS.

## Sync modes

<SyncModes />

All tables support incremental sync using Qualys' server-side date filters, so ongoing syncs only pull hosts, detections, scans, and KnowledgeBase entries that changed since the last run.

## Configuration

<SourceParameters />

## Supported tables

<SourceTables />

## Troubleshooting

- **Sync fails immediately with an authentication error**: check the username and password, and that the user has API access enabled in Qualys.
- **Syncs are slow or pause periodically**: Qualys enforces per-subscription API rate limits (about 300 calls/hour by default). The connector waits the amount Qualys asks for and resumes automatically.
- **The knowledge_base table errors while other tables sync fine**: the KnowledgeBase download option isn't enabled on your subscription.

<TroubleshootingLink />

🤖 Agent context

Autonomy: Fully autonomous

Implemented by PostHog Code (Claude) as part of the batch source-implementation effort stacked on the scaffold PR. Skills invoked: /implementing-warehouse-sources, /documenting-warehouse-sources, /writing-tests. Key decisions: bespoke XML transport instead of rest_source.RESTClient (XML responses, truncation pagination); sort_mode=\"desc\" since batches are id-ordered rather than time-ordered; nested XML serialized to JSON strings for Arrow type stability; knowledge_base off by default (subscription add-on); create-time credential probe accepts 403/409 so a missing asset-list scope doesn't block source creation.


Created with PostHog Code

@github-actions

Copy link
Copy Markdown
Contributor

Hey @Gilbert09! 👋

It looks like your git author email on this PR isn't your @posthog.com address (owerstom@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 15, 2026 16:34
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Backend coverage — 96.0% of changed backend lines covered — 14 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 96.0% (435 / 449)

File Patch Uncovered changed lines
products/warehouse_sources/backend/temporal/data_imports/sources/qualys_vmdr/qualys_vmdr.py 93.4% 79, 83, 122, 129–131, 145–148, 196–197
products/warehouse_sources/backend/temporal/data_imports/sources/qualys_vmdr/source.py 94.1% 98, 102

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29497320496 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
tasks █████████████░░░░░░░ 67.4% 25,805 / 38,278
signals ████████████████░░░░ 79.1% 19,037 / 24,073
data_modeling ████████████████░░░░ 80.0% 4,834 / 6,045
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
notebooks █████████████████░░░ 84.3% 6,343 / 7,520
agent_platform █████████████████░░░ 84.7% 3,273 / 3,862
cohorts █████████████████░░░ 86.1% 4,022 / 4,671
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.3% 6,891 / 7,800
conversations ██████████████████░░ 88.9% 16,129 / 18,133
dashboards ██████████████████░░ 89.1% 5,719 / 6,418
mcp_analytics ██████████████████░░ 89.1% 2,502 / 2,807
error_tracking ██████████████████░░ 89.6% 9,790 / 10,925
alerts ██████████████████░░ 89.9% 3,638 / 4,046
engineering_analytics ██████████████████░░ 90.1% 5,105 / 5,665
streamlit_apps ██████████████████░░ 90.4% 2,499 / 2,764
slack_app ██████████████████░░ 90.6% 9,511 / 10,503
marketing_analytics ██████████████████░░ 90.8% 11,514 / 12,684
product_analytics ██████████████████░░ 91.2% 5,663 / 6,209
data_warehouse ██████████████████░░ 92.1% 18,133 / 19,683
workflows ██████████████████░░ 92.4% 5,148 / 5,574
web_analytics ███████████████████░ 92.7% 13,624 / 14,691
ai_observability ███████████████████░ 92.8% 14,868 / 16,019
surveys ███████████████████░ 92.9% 5,687 / 6,120
posthog_ai ███████████████████░ 93.2% 1,322 / 1,418
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
early_access_features ███████████████████░ 93.8% 848 / 904
endpoints ███████████████████░ 94.1% 8,606 / 9,143
skills ███████████████████░ 94.4% 2,827 / 2,995
revenue_analytics ███████████████████░ 94.5% 3,598 / 3,809
review_hog ███████████████████░ 94.5% 6,557 / 6,935
logs ███████████████████░ 95.3% 9,528 / 9,994
experiments ███████████████████░ 95.6% 24,234 / 25,351
replay_vision ███████████████████░ 95.7% 13,354 / 13,952
annotations ███████████████████░ 96.2% 732 / 761
feature_flags ███████████████████░ 96.3% 16,118 / 16,741
warehouse_sources ███████████████████░ 96.3% 240,055 / 249,249
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
data_catalog ███████████████████░ 97.1% 2,035 / 2,095
customer_analytics ███████████████████░ 97.2% 7,480 / 7,698

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

Django migration risk — no migrations to analyze

No Django migrations need risk analysis.

ℹ️ Docs preview — preview build triggered

Docs from this PR will be published at posthog.com.

Project Preview Updated (UTC)
posthog.com Open preview Jul 15, 2026, 9:00 PM

The preview should be ready in about 10 minutes. Open the preview at /handbook/engineering/.

@veria-ai

veria-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 2 · PR risk: 0/10

@trunk-io

trunk-io Bot commented Jul 15, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@danielcarletti danielcarletti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Implements the Qualys VMDR warehouse source: XML transport with truncation-URL pagination, resumable state, server-side incremental filters for hosts, host_list_detection, scans, and knowledge_base, plus canonical table descriptions and tests.

Generated-By: PostHog Code
Task-Id: 6f8fb7c3-3615-40d9-a6c3-2bb5d428ef0f
Parse Qualys XML with defusedxml (XXE/entity-expansion protection, flagged by semgrep), and fix mypy errors in the transport tests (unwrapped tenacity call and fake resumable manager typing, following the established sibling-test idioms).

Generated-By: PostHog Code
Task-Id: 6f8fb7c3-3615-40d9-a6c3-2bb5d428ef0f
Validate the user-supplied API server with is_url_allowed at credential validation and on every sync (defense-in-depth alongside the Smokescreen egress proxy), and disable redirects on the tracked session so the basic-auth credential stays on the validated host. Follows the established active_campaign pattern. Credential validation now returns a specific error message.

Generated-By: PostHog Code
Task-Id: 6f8fb7c3-3615-40d9-a6c3-2bb5d428ef0f
…mport

The use-defused-xml semgrep rule also flags the type-only `from xml.etree.ElementTree import Element` import. Only parsing carries XXE risk (and all parsing goes through defusedxml), so suppress with the same justified pragma used in posthog/udf_versioner.py.

Generated-By: PostHog Code
Task-Id: 6f8fb7c3-3615-40d9-a6c3-2bb5d428ef0f
@Gilbert09
Gilbert09 force-pushed the posthog-code/implement-qualys-vmdr-source branch from afa3130 to 2e634f2 Compare July 15, 2026 20:59
@Gilbert09
Gilbert09 changed the base branch from tom/scaffold-100-eng-support-sources to master July 15, 2026 20:59
…time

The Qualys API server is user-supplied, so a malicious or misconfigured host
could stream an unbounded or slow-dripped body and exhaust a shared worker.
Read sync responses with stream=True under byte and wall-clock caps, and open
the credential probe with stream=True, closing it without ever reading the body.

Generated-By: PostHog Code
Task-Id: ae6686e3-e27a-4ffe-bfba-e799b38b1c65
Generated-By: PostHog Code
Task-Id: ae6686e3-e27a-4ffe-bfba-e799b38b1c65
@talyn-app
talyn-app Bot merged commit e5d9e9b into master Jul 16, 2026
233 checks passed
@talyn-app
talyn-app Bot deleted the posthog-code/implement-qualys-vmdr-source branch July 16, 2026 13:15
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-16 13:48 UTC Run
prod-us ✅ Deployed 2026-07-16 14:13 UTC Run
prod-eu ✅ Deployed 2026-07-16 14:13 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants