feat(integration-platform): add CybeDefend security scanning integration - #3567
feat(integration-platform): add CybeDefend security scanning integration#3567FlorentinLedy wants to merge 9 commits into
Conversation
The boolean branch already fell back to `variable.default`; the select branch did not, so every dropdown with a declared default rendered empty. GitHub's Dependabot severity threshold declares `default: 'high'` and was affected the same way.
Adds an optional `showIf` to a manifest's credential fields, so an integration can ask for input that only some choices need, such as a dedicated-tenant name or a self-hosted host, without showing it to everyone. A hidden field is neither rendered, nor validated, nor submitted. The last one matters most: 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, the connect screen and the reconfigure dialog, since duplicating the rule is how they drift. No shipped manifest declares `showIf`, so this is inert for every existing integration.
Reports one result per CybeDefend project for two scan types, so a project's
security posture becomes evidence against the compliance controls:
- Code Scanning (SAST) -> TASK_TEMPLATES.sanitizedInputs
- Dependency Scanning (SCA) -> TASK_TEMPLATES.secureCode, the task that
asks for "dependabot or its equivalent"
CybeDefend is deployed per region, so the form takes a region selector (eu, us,
or a dedicated tenant) and derives every URL from it. A dedicated tenant is
named, never typed as a URL: `api-${tenant}.cybedefend.com` is interpolation,
and `x@evil.com/` would resolve to evil.com, posting the customer's token to an
attacker. The tenant is restricted to a DNS label, at the form and again before
the interpolation; the server does not enforce a manifest's validation schema,
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, and it reaches no log, error message or result.
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.
e7a857d to
a3c0964
Compare
There was a problem hiding this comment.
6 issues found and verified against the latest diff
Confidence score: 2/5
packages/integration-platform/src/manifests/cybedefend/client.tscan accept malformed finding exports, then drop incomplete findings or throw on a non-string severity; validate each finding payload before processing it.packages/integration-platform/src/manifests/cybedefend/regions.tsallows 59–63-character tenants that produce invalidauth-${tenant}DNS labels, preventing authentication; cap tenant length at 58 characters.packages/integration-platform/src/manifests/cybedefend/checks/project-findings-check.tstreats/user/profilefailures as an empty project list, which can hide clean projects and produce incomplete or empty results; preserve the failure or explicitly surface partial coverage.packages/integration-platform/src/manifests/cybedefend/auth.tsbypasses runner retries for transient authentication failures, whileEmptyStateOnboarding.tsxandConnectionVariablesForm.tsxcan submit hidden or unstored variable values; use a retry-capable transport and apply shared visibility/default-state handling before save.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/integration-platform/src/manifests/cybedefend/client.ts">
<violation number="1" location="packages/integration-platform/src/manifests/cybedefend/client.ts:94">
P1: When the export contains a malformed finding, this guard accepts it because it validates only the array container. The check then drops findings missing `finding_type` or throws on a non-string `severity`; validate each required finding field before returning the page.</violation>
</file>
<file name="apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/EmptyStateOnboarding.tsx">
<violation number="1" location="apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/EmptyStateOnboarding.tsx:369">
P2: Cloud providers still bypass `showIf`: `CloudSetup` renders conditional fields, validates hidden required fields, and submits their stale values. Apply the shared visibility predicate to the cloud path as well.</violation>
</file>
<file name="packages/integration-platform/src/manifests/cybedefend/regions.ts">
<violation number="1" location="packages/integration-platform/src/manifests/cybedefend/regions.ts:16">
P2: When a dedicated tenant has 59–63 characters, `TENANT_PATTERN` accepts it but `urlsFor` creates an invalid `auth-${tenant}` DNS label, so the connection cannot authenticate. Cap the tenant length at 58 characters and apply the same bound in the credential schema.</violation>
</file>
<file name="packages/integration-platform/src/manifests/cybedefend/checks/project-findings-check.ts">
<violation number="1" location="packages/integration-platform/src/manifests/cybedefend/checks/project-findings-check.ts:243">
P2: When `/user/profile` fails, this returns an empty project list and the check continues with partial coverage. Projects with no findings are then omitted, so an all-clean organization can emit no results without a failed run; fail or explicitly surface the project-list error instead of silently treating it as empty.</violation>
</file>
<file name="packages/integration-platform/src/manifests/cybedefend/auth.ts">
<violation number="1" location="packages/integration-platform/src/manifests/cybedefend/auth.ts:52">
P2: When a CybeDefend run encounters a transient DNS, socket, rate-limit, or 5xx failure during authentication, this raw fetch fails the check immediately without the runner's retry policy. Supply a retry-capable transport to the manifest or add equivalent retry handling for the custom authentication request.</violation>
</file>
<file name="apps/app/src/components/integrations/ConnectionVariablesForm.tsx">
<violation number="1" location="apps/app/src/components/integrations/ConnectionVariablesForm.tsx:166">
P2: The fallback only changes what the dropdown renders; it does not persist the default into `variableValues`. On save, `ManageIntegrationDialog` sends `variableValues` verbatim, and that state is seeded only for variables with an existing `currentValue`. For a new connection the dropdown shows the declared default while the key stays absent from the payload, so the claimed empty-value fix is display-only unless the operator re-selects the value. Seed each select's default into `variableValues` (or commit it on change/save) so the rendered value matches what is actually stored.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| const envelope = payload as { data?: unknown }; | ||
| const page = (envelope?.data ?? payload) as Partial<CybeDefendFindingsPage>; | ||
|
|
||
| if (!Array.isArray(page?.findings) || typeof page?.has_more !== 'boolean') { |
There was a problem hiding this comment.
P1: When the export contains a malformed finding, this guard accepts it because it validates only the array container. The check then drops findings missing finding_type or throws on a non-string severity; validate each required finding field before returning the page.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/cybedefend/client.ts, line 94:
<comment>When the export contains a malformed finding, this guard accepts it because it validates only the array container. The check then drops findings missing `finding_type` or throws on a non-string `severity`; validate each required finding field before returning the page.</comment>
<file context>
@@ -0,0 +1,161 @@
+ const envelope = payload as { data?: unknown };
+ const page = (envelope?.data ?? payload) as Partial<CybeDefendFindingsPage>;
+
+ if (!Array.isArray(page?.findings) || typeof page?.has_more !== 'boolean') {
+ throw new Error('Unexpected response from the CybeDefend findings export.');
+ }
</file context>
| const hasConfigurableFields = fields.length > 0; | ||
|
|
||
| const visibleFields = useMemo( | ||
| () => visibleCredentialFields(fields, credentials), |
There was a problem hiding this comment.
P2: Cloud providers still bypass showIf: CloudSetup renders conditional fields, validates hidden required fields, and submits their stale values. Apply the shared visibility predicate to the cloud path as well.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/app/(app)/[orgId]/integrations/[slug]/components/EmptyStateOnboarding.tsx, line 369:
<comment>Cloud providers still bypass `showIf`: `CloudSetup` renders conditional fields, validates hidden required fields, and submits their stale values. Apply the shared visibility predicate to the cloud path as well.</comment>
<file context>
@@ -363,7 +364,13 @@ function CredentialSetup({
- const hasConfigurableFields = fields.length > 0;
+
+ const visibleFields = useMemo(
+ () => visibleCredentialFields(fields, credentials),
+ [fields, credentials],
+ );
</file context>
| * `x@evil.com/` would yield `https://api-x@evil.com/.cybedefend.com`, whose | ||
| * host is evil.com. The token would then be posted to an attacker. | ||
| */ | ||
| const TENANT_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; |
There was a problem hiding this comment.
P2: When a dedicated tenant has 59–63 characters, TENANT_PATTERN accepts it but urlsFor creates an invalid auth-${tenant} DNS label, so the connection cannot authenticate. Cap the tenant length at 58 characters and apply the same bound in the credential schema.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/cybedefend/regions.ts, line 16:
<comment>When a dedicated tenant has 59–63 characters, `TENANT_PATTERN` accepts it but `urlsFor` creates an invalid `auth-${tenant}` DNS label, so the connection cannot authenticate. Cap the tenant length at 58 characters and apply the same bound in the credential schema.</comment>
<file context>
@@ -0,0 +1,67 @@
+ * `x@evil.com/` would yield `https://api-x@evil.com/.cybedefend.com`, whose
+ * host is evil.com. The token would then be posted to an attacker.
+ */
+const TENANT_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
+
+export interface CybeDefendRegionUrls {
</file context>
| apiBaseUrl, | ||
| logtoEndpoint, | ||
| personalAccessToken, | ||
| fetchImpl = globalThis.fetch as unknown as FetchImpl, |
There was a problem hiding this comment.
P2: When a CybeDefend run encounters a transient DNS, socket, rate-limit, or 5xx failure during authentication, this raw fetch fails the check immediately without the runner's retry policy. Supply a retry-capable transport to the manifest or add equivalent retry handling for the custom authentication request.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/cybedefend/auth.ts, line 52:
<comment>When a CybeDefend run encounters a transient DNS, socket, rate-limit, or 5xx failure during authentication, this raw fetch fails the check immediately without the runner's retry policy. Supply a retry-capable transport to the manifest or add equivalent retry handling for the custom authentication request.</comment>
<file context>
@@ -0,0 +1,86 @@
+ apiBaseUrl,
+ logtoEndpoint,
+ personalAccessToken,
+ fetchImpl = globalThis.fetch as unknown as FetchImpl,
+}: ExchangePersonalAccessTokenOptions): Promise<string> => {
+ const clientId = await fetchCliAppId({ apiBaseUrl, fetchImpl });
</file context>
| headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }, | ||
| }); | ||
|
|
||
| if (!response.ok) return []; |
There was a problem hiding this comment.
P2: When /user/profile fails, this returns an empty project list and the check continues with partial coverage. Projects with no findings are then omitted, so an all-clean organization can emit no results without a failed run; fail or explicitly surface the project-list error instead of silently treating it as empty.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/cybedefend/checks/project-findings-check.ts, line 243:
<comment>When `/user/profile` fails, this returns an empty project list and the check continues with partial coverage. Projects with no findings are then omitted, so an all-clean organization can emit no results without a failed run; fail or explicitly surface the project-list error instead of silently treating it as empty.</comment>
<file context>
@@ -0,0 +1,249 @@
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
+ });
+
+ if (!response.ok) return [];
+
+ const payload = (await response.json()) as { myAccessibleProjects?: AccessibleProject[] };
</file context>
| ) : variable.type === 'select' ? ( | ||
| <Select | ||
| value={String(variableValues[variable.id] ?? '')} | ||
| value={String(variableValues[variable.id] ?? variable.default ?? '')} |
There was a problem hiding this comment.
P2: The fallback only changes what the dropdown renders; it does not persist the default into variableValues. On save, ManageIntegrationDialog sends variableValues verbatim, and that state is seeded only for variables with an existing currentValue. For a new connection the dropdown shows the declared default while the key stays absent from the payload, so the claimed empty-value fix is display-only unless the operator re-selects the value. Seed each select's default into variableValues (or commit it on change/save) so the rendered value matches what is actually stored.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/src/components/integrations/ConnectionVariablesForm.tsx, line 166:
<comment>The fallback only changes what the dropdown renders; it does not persist the default into `variableValues`. On save, `ManageIntegrationDialog` sends `variableValues` verbatim, and that state is seeded only for variables with an existing `currentValue`. For a new connection the dropdown shows the declared default while the key stays absent from the payload, so the claimed empty-value fix is display-only unless the operator re-selects the value. Seed each select's default into `variableValues` (or commit it on change/save) so the rendered value matches what is actually stored.</comment>
<file context>
@@ -163,7 +163,7 @@ export function ConnectionVariablesFields({
) : variable.type === 'select' ? (
<Select
- value={String(variableValues[variable.id] ?? '')}
+ value={String(variableValues[variable.id] ?? variable.default ?? '')}
onValueChange={(value) => {
if (value === null) return;
</file context>
…dings export rank() called .trim() on raw provider data, so a finding whose severity is null aborted the whole check run and no project was reported. A check that reports nothing looks like a check that passed. Non-strings now take the unknown rank they were always meant to have: counted apart, warned about, and unable to breach any threshold.
… label
The pattern allowed 63 characters, but the longest derived label is
auth-${tenant}, so a tenant of 59 to 63 characters passed validation and could
never authenticate. Capped at 58.
The pattern is now exported from regions.ts and imported by the credential
schema instead of being written twice. Both guard the same host interpolation,
and the run-time copy is the only real barrier, so they must not drift.
A proxy answering 200 with HTML made response.json() throw a raw SyntaxError, which tells the operator nothing about the region or the token. The decode now falls through to the existing message, with the status attached.
The non-empty check ran before the visibility filter, so a value left in a hidden field satisfied it while the filtered payload came out empty. The dialog then reported "Credentials updated" without updating anything. The check now runs on what is actually about to be sent.
The test clicked connect without setting the required region, so validation returned before createConnection was reached. It asserted only that the hidden field renders no error of its own, never that submission goes through. It now sets the region first and asserts the call and its payload.
There was a problem hiding this comment.
1 issue found across 10 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/integration-platform/src/manifests/cybedefend/regions.ts">
<violation number="1" location="packages/integration-platform/src/manifests/cybedefend/regions.ts:19">
P3: The new 58-character ceiling is not mentioned in the validation messages, so a tenant rejected purely for length gets a misleading error. A 59-char name like `aaa…` passes the character rules but fails on length; both `resolveRegion` here and the credential schema in credentials.ts tell the user only that the name 'may only contain lowercase letters, digits and hyphens, and cannot start or end with a hyphen', which is false for an all-alpha tenant. Add the length cap to both messages so users with long dedicated-tenant names understand why their entry is rejected.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| * Capped at 58 so the longest derived label, `auth-${tenant}`, stays within the | ||
| * 63-character DNS limit. | ||
| */ | ||
| export const TENANT_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,56}[a-z0-9])?$/; |
There was a problem hiding this comment.
P3: The new 58-character ceiling is not mentioned in the validation messages, so a tenant rejected purely for length gets a misleading error. A 59-char name like aaa… passes the character rules but fails on length; both resolveRegion here and the credential schema in credentials.ts tell the user only that the name 'may only contain lowercase letters, digits and hyphens, and cannot start or end with a hyphen', which is false for an all-alpha tenant. Add the length cap to both messages so users with long dedicated-tenant names understand why their entry is rejected.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/cybedefend/regions.ts, line 19:
<comment>The new 58-character ceiling is not mentioned in the validation messages, so a tenant rejected purely for length gets a misleading error. A 59-char name like `aaa…` passes the character rules but fails on length; both `resolveRegion` here and the credential schema in credentials.ts tell the user only that the name 'may only contain lowercase letters, digits and hyphens, and cannot start or end with a hyphen', which is false for an all-alpha tenant. Add the length cap to both messages so users with long dedicated-tenant names understand why their entry is rejected.</comment>
<file context>
@@ -12,8 +12,11 @@ const PUBLIC_REGIONS = ['eu', 'us'] as const;
+ * 63-character DNS limit.
*/
-const TENANT_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
+export const TENANT_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,56}[a-z0-9])?$/;
export interface CybeDefendRegionUrls {
</file context>
…ected The length cap was added without touching the message that describes the rule, so a 59-letter name was told it may only contain lowercase letters, which is exactly what it was. The rule is now written once and shared by the schema and the run-time guard, so the two cannot describe different constraints.
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Confidence score: 5/5
- In
packages/integration-platform/src/manifests/cybedefend/regions.ts,MAX_TENANT_LENGTHand the{0,56}bound inTENANT_PATTERNare separate sources of truth, so future changes could allow a tenant value that validation rejects or produce a misleading error message—derive the pattern limit from the constant or otherwise centralize the constraint.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/integration-platform/src/manifests/cybedefend/regions.ts">
<violation number="1" location="packages/integration-platform/src/manifests/cybedefend/regions.ts:19">
P3: MAX_TENANT_LENGTH is presented as the authoritative cap but is only used to build the error message; the actual limit is hardcoded as `{0,56}` inside TENANT_PATTERN. Today both agree (58), but the two sources of truth can drift: changing one makes the shared TENANT_RULE message claim a different limit than the regex enforces. Derive the pattern from the constant (e.g. `new RegExp('^[a-z0-9](?:[a-z0-9-]{0,' + (MAX_TENANT_LENGTH - 2) + '}[a-z0-9])?$')`) or add a comment tying them.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| * Capped at 58 so the longest derived label, `auth-${tenant}`, stays within the | ||
| * 63-character DNS limit. | ||
| */ | ||
| export const MAX_TENANT_LENGTH = 58; |
There was a problem hiding this comment.
P3: MAX_TENANT_LENGTH is presented as the authoritative cap but is only used to build the error message; the actual limit is hardcoded as {0,56} inside TENANT_PATTERN. Today both agree (58), but the two sources of truth can drift: changing one makes the shared TENANT_RULE message claim a different limit than the regex enforces. Derive the pattern from the constant (e.g. new RegExp('^[a-z0-9](?:[a-z0-9-]{0,' + (MAX_TENANT_LENGTH - 2) + '}[a-z0-9])?$')) or add a comment tying them.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integration-platform/src/manifests/cybedefend/regions.ts, line 19:
<comment>MAX_TENANT_LENGTH is presented as the authoritative cap but is only used to build the error message; the actual limit is hardcoded as `{0,56}` inside TENANT_PATTERN. Today both agree (58), but the two sources of truth can drift: changing one makes the shared TENANT_RULE message claim a different limit than the regex enforces. Derive the pattern from the constant (e.g. `new RegExp('^[a-z0-9](?:[a-z0-9-]{0,' + (MAX_TENANT_LENGTH - 2) + '}[a-z0-9])?$')`) or add a comment tying them.</comment>
<file context>
@@ -16,8 +16,12 @@ const PUBLIC_REGIONS = ['eu', 'us'] as const;
* Capped at 58 so the longest derived label, `auth-${tenant}`, stays within the
* 63-character DNS limit.
*/
+export const MAX_TENANT_LENGTH = 58;
+
export const TENANT_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,56}[a-z0-9])?$/;
</file context>
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:
taskMappingTASK_TEMPLATES.sanitizedInputsTASK_TEMPLATES.secureCodeThree 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 integrationNo issue number:
CONTRIBUTING.mdasks 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
selectvariable declaring adefaultrendered as an empty dropdown, so theoperator could not tell which value the check would apply. The
booleanbranchof the same component already fell back to
variable.default; theselectbranch did not:
This affects shipped integrations: GitHub's Dependabot
alert_severity_thresholddeclares
default: 'high'and was showing empty.Commit 2,
showIfon credential fieldsAdds an optional
showIfso 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 existingintegration.
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 nofree-text host field, and that is deliberate.
api-${tenant}.cybedefend.comis string interpolation, so a tenant carrying@or
/relocates the host entirely:x@evil.com/yieldshttps://api-x@evil.com/.cybedefend.com, whose host isevil.com, and theintegration 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, sothat 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:
projects are read from the account's project list. Otherwise they would be
invisible rather than green.
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
packages/integrationshandler pathVerification
packages/integration-platform: 80 new tests for the manifest, coveringregion 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 selectwith 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 typecheckclean; no file in this diff is reported by Prettier
both checks return 17 results, covering 592 SAST and 1090 SCA findings
bun run buildandbun run lintfail onmainbefore this branch(
packages/dbhas an implicitanyinbackfill-framework-versions.ts;packages/analyticsandpackages/billingare unformatted, including committeddist/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
container,iac,cicdandsecretfindings; onlySAST and SCA map to tasks that exist today.
secretwould fitTASK_TEMPLATES.secureSecrets.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
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.auth-label stays valid.Written for commit 83c18cd. Summary will update on new commits.