feat(integration-platform): add CybeDefend security scanning integration
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:
| Check | taskMapping | Task |
|---|---|---|
| Code Scanning (SAST) | TASK_TEMPLATES.sanitizedInputs | Sanitized Inputs |
| Dependency Scanning (SCA) | TASK_TEMPLATES.secureCode | Secure Code, the "dependabot or its equivalent" task |
Three commits, reviewable in order and independently revertable:
fix(app): preselect a select variable's declared defaultfeat(integration-platform): let a credential field depend on anotherfeat(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/integrationshandler 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 verifyinpackages/integration-platformvalidates the manifest - β
@trycompai/integration-platformand@trycompai/appbuild 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,cicdandsecretfindings; only
SAST and SCA map to tasks that exist today.secretwould 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.