feat(integration-platform): add CybeDefend security scanning integration

FL
2mo ago
+2129-50

What & why

Adds CybeDefend as a code-based integration manifest, plus two fixes to the
shared credential-field machinery it depends on.

CybeDefend is an application security platform (SAST, SCA, container, IaC, CI/CD,
secrets). The integration pulls its findings export and reports one result per
project per scan type
, so a project's security posture becomes evidence against
existing compliance tasks:

ChecktaskMappingTask
Code Scanning (SAST)TASK_TEMPLATES.sanitizedInputsSanitized Inputs
Dependency Scanning (SCA)TASK_TEMPLATES.secureCodeSecure Code, the "dependabot or its equivalent" task

Three commits, reviewable in order and independently revertable:

  1. fix(app): preselect a select variable's declared default
  2. feat(integration-platform): let a credential field depend on another
  3. feat(integration-platform): add CybeDefend security scanning integration

No issue number: CONTRIBUTING.md asks feature requests to wait for
🚨 needs approval. Happy to open one and hold this PR. Commit 1 is a bug fix,
which the same section exempts.

Commit 1, select variables ignored their declared default

A select variable declaring a default rendered as an empty dropdown, so the
operator could not tell which value the check would apply. The boolean branch
of the same component already fell back to variable.default; the select
branch did not:

// select   (before)
value={String(variableValues[variable.id] ?? '')}
// boolean  (already correct)
value={String(variableValues[variable.id] ?? variable.default ?? 'false')}

This affects shipped integrations: GitHub's Dependabot alert_severity_threshold
declares default: 'high' and was showing empty.

Commit 2, showIf on credential fields

Adds an optional showIf so a manifest can ask for input only some choices need.
A hidden field is neither rendered, nor validated, nor submitted. The last
one matters: a value typed into a field that was then hidden again is not part of
the operator's intent, yet would still be encrypted and stored.

Both surfaces share one predicate rather than a copy each (connect screen and
reconfigure dialog), since duplicating the rule is how they drift.

No shipped manifest declares showIf, so this is inert for every existing
integration.

Commit 3, the manifest

CybeDefend is deployed per region, so the form takes a region selector
(eu, us, or a dedicated tenant) and derives every URL from it. There is no
free-text host field, and that is deliberate.

api-${tenant}.cybedefend.com is string interpolation, so a tenant carrying @
or / relocates the host entirely: x@evil.com/ yields
https://api-x@evil.com/.cybedefend.com, whose host is evil.com, and the
integration would post the customer's access token there. Tenant names are
restricted to a DNS label, validated at the form and again before the
interpolation. The server does not enforce a manifest's validationSchema, so
that second check is the only real barrier.

Auth is a personal access token exchanged for a short-lived API token on every
run. Only the token is stored; it reaches no log, error message or emitted result.

Two behaviours worth knowing:

  • A project with no findings never appears in the findings feed, so clean
    projects are read from the account's project list. Otherwise they would be
    invisible rather than green.
  • A severity outside the known set is counted apart and warned about rather than
    folded into the weakest level. That fold is how findings arriving with an empty
    severity stayed below every threshold and went unreported.

Explicitly NOT touched

  • The legacy packages/integrations handler path
  • Any existing manifest, check, or task mapping
  • Auth, RBAC, API schemas, DB schema. No migration
  • The dynamic-integration DSL and its runner

Verification

  • βœ… packages/integration-platform: 80 new tests for the manifest, covering
    region and tenant derivation (14 adversarial host-injection cases), the PAT
    exchange, cursor pagination across pages, and the check itself
  • βœ… apps/app: 31 tests across the two shared fixes, including that a select
    with no default stays empty and that a hidden field's value is not submitted
  • βœ… bun run verify in packages/integration-platform validates the manifest
  • βœ… @trycompai/integration-platform and @trycompai/app build and typecheck
    clean; no file in this diff is reported by Prettier
  • βœ… End to end against a real organization (17 projects, 2063 open findings):
    both checks return 17 results, covering 592 SAST and 1090 SCA findings

bun run build and bun run lint fail on main before this branch
(packages/db has an implicit any in backfill-framework-versions.ts;
packages/analytics and packages/billing are unformatted, including committed
dist/ files), so I verified the touched packages instead.

Impact

Additive. New integration, one shared bug fixed, one shared capability added that
no existing manifest uses. Existing connections and check results are untouched.

Follow-ups, deliberately out of scope

  • CybeDefend also reports container, iac, cicd and secret findings; only
    SAST and SCA map to tasks that exist today. secret would fit
    TASK_TEMPLATES.secureSecrets.
  • The findings export has no server-side scan-type filter, so each check pulls
    the organization's open findings once and narrows locally, two passes per run.

Summary by cubic

Adds the CybeDefend security scanning integration and two improvements to shared integration machinery. CybeDefend reports one result per project for SAST and SCA findings, mapped to the Sanitized Inputs and Secure Code compliance tasks.

What changed

  • Select variables now preselect their declared default instead of rendering empty; GitHub's Dependabot severity threshold was affected.
  • Credential fields can declare showIf; hidden fields are not rendered, validated, or submitted. A credential save with nothing left to send is now rejected instead of falsely reporting success.
  • Region selector derives all URLs; dedicated tenant names are validated as DNS labels to prevent host injection, capped at 58 characters so the derived auth- label stays valid.
  • Personal access tokens are exchanged for short-lived API tokens on each run and never logged; a non-JSON or failed exchange reports a clear message.
  • Clean projects come from the account project list since they don't appear in the findings feed.
  • Unrecognized severities are counted and warned about, not folded into the weakest level; a non-string severity no longer aborts the run.