Skip to content

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

Open
FlorentinLedy wants to merge 9 commits into
trycompai:mainfrom
FlorentinLedy:feat/cybedefend-integration
Open

feat(integration-platform): add CybeDefend security scanning integration#3567
FlorentinLedy wants to merge 9 commits into
trycompai:mainfrom
FlorentinLedy:feat/cybedefend-integration

Conversation

@FlorentinLedy

@FlorentinLedy FlorentinLedy commented Sep 6, 2026

Copy link
Copy Markdown

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:

  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.

Written for commit 83c18cd. Summary will update on new commits.

Review in cubic

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.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

CLAassistant commented Sep 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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.
@FlorentinLedy
FlorentinLedy force-pushed the feat/cybedefend-integration branch from e7a857d to a3c0964 Compare September 6, 2026 10:13

@cubic-dev-ai cubic-dev-ai Bot 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.

6 issues found and verified against the latest diff

Confidence score: 2/5

  • packages/integration-platform/src/manifests/cybedefend/client.ts can 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.ts allows 59–63-character tenants that produce invalid auth-${tenant} DNS labels, preventing authentication; cap tenant length at 58 characters.
  • packages/integration-platform/src/manifests/cybedefend/checks/project-findings-check.ts treats /user/profile failures 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.ts bypasses runner retries for transient authentication failures, while EmptyStateOnboarding.tsx and ConnectionVariablesForm.tsx can 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') {

@cubic-dev-ai cubic-dev-ai Bot Sep 6, 2026

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.

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>
Fix with cubic

const hasConfigurableFields = fields.length > 0;

const visibleFields = useMemo(
() => visibleCredentialFields(fields, credentials),

@cubic-dev-ai cubic-dev-ai Bot Sep 6, 2026

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.

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>
Fix with cubic

* `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])?$/;

@cubic-dev-ai cubic-dev-ai Bot Sep 6, 2026

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.

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>
Fix with cubic

apiBaseUrl,
logtoEndpoint,
personalAccessToken,
fetchImpl = globalThis.fetch as unknown as FetchImpl,

@cubic-dev-ai cubic-dev-ai Bot Sep 6, 2026

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.

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>
Fix with cubic

headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
});

if (!response.ok) return [];

@cubic-dev-ai cubic-dev-ai Bot Sep 6, 2026

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.

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>
Fix with cubic

) : variable.type === 'select' ? (
<Select
value={String(variableValues[variable.id] ?? '')}
value={String(variableValues[variable.id] ?? variable.default ?? '')}

@cubic-dev-ai cubic-dev-ai Bot Sep 6, 2026

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.

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>
Fix with cubic

Comment thread packages/integration-platform/src/manifests/cybedefend/auth.ts Outdated
Comment thread packages/integration-platform/src/manifests/cybedefend/credentials.ts Outdated
…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.

@cubic-dev-ai cubic-dev-ai Bot 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.

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])?$/;

@cubic-dev-ai cubic-dev-ai Bot Sep 8, 2026

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.

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>
Fix with cubic

…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.

@cubic-dev-ai cubic-dev-ai Bot 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.

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_LENGTH and the {0,56} bound in TENANT_PATTERN are 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;

@cubic-dev-ai cubic-dev-ai Bot Sep 8, 2026

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.

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>
Fix with cubic

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