Commit Graph

950 Commits

Author SHA1 Message Date
Cursor Agent
5ea732a97f Resolve SCIM export profiles from event path IDs
SCIM events store user_name, not identity_id; profile GIDs
in /Users/{id} paths drive LoadExistingByIDs and identity
email lookup instead of batching by user_name.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:13:17 +00:00
Cursor Agent
c5b1f210d5 Harden log export CSV and fix profile loader boundary
Load SCIM profile emails via Identities in the export
layer, drop the cross-table join, sanitize formula-leading
cells, and fall back to validated userName when profile email
is missing. Drop redundant gid array casts in LoadByIDs.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
6906e0ff5b Separate SCIM export email from event user name
Load identity email for matched profiles and drop parsing
userName as email; user_name column stays the event value.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
6cbf16814c Use coredata models in log export batch loaders
Drop export-only row types; load Identity, PersonalAPIKey,
and MembershipProfile through the usual slice LoadByIDs helpers.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
e4d86f0f9e Add narrow audit log actor loaders for CSV export
Replace full Identity and PersonalAPIKey batch loads with
rows that select only id, email, name fields used in export.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
7bba43b902 Select only SCIM export profile columns by user name
The batch loader no longer scans into MembershipProfile with
placeholder email and organization name columns from dropped
identity and organization joins.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
ced25bad32 Load SCIM export profiles without identity join
Membership profile rows already carry SCIM userName and
name fields; export email comes from userName, not login
identity email.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
b715107e6c Drop empty-input guards from batch loaders
ANY(@ids) with an empty array is valid in Postgres and
CollectRows already yields an empty slice; the early nil
assignments added noise without changing callers.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Cursor Agent
5cd407bd86 Emit audit and SCIM log exports as CSV
JSONL was awkward in spreadsheets and SIEM imports. Write
tab-separated-friendly CSV with organization name on every row,
resolve audit actors to email or API key name, and enrich SCIM rows
with profile email and display name when available.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-30 10:12:47 +00:00
Émile Ré
eecf8a1d97 Make profile state filters multi-value
Address PR review feedback on the profile-state split: SAML sign-in now
activates a pending profile, deactivation counts owners against the
profile's own organization to close a last-owner bypass, the migration
leaves historical activated_at/deactivated_at NULL rather than
fabricating timestamps, and pending members are no longer rendered with
the deactivated (faded) styling.

Drop the single-value state filter in favor of the multi-value states
across the profile and signatory surfaces. Remove ProfileFilter.state
(only states[] remains) and convert the signatures profileState filter
to profileStates. Turn the console people filter, the CLI
"user list --state" flag, and the n8n listUsers and getAllSignatures
state inputs into multi-select controls, where an empty selection means
all states.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-30 09:19:42 +02:00
Émile Ré
4a276e3ef7 Split inactive profile state
Replace the binary profile ACTIVE/INACTIVE model with PENDING, ACTIVE,
and DEACTIVATED so invited-but-not-yet-activated members remain
assignable to assets, data, and risks instead of being treated like
deactivated users.

Add activated_at/deactivated_at timestamps and Mark* lifecycle helpers,
and update every transition (create, invite/re-invite, activation,
archive, SCIM, SAML, sessions, compliance-portal grant) to the new
states. Expose a multi-state states[] filter across coredata, GraphQL,
MCP, and the console owner pickers, which now request ACTIVE and
PENDING members.

A migration renames the membership_state enum, classifies existing
inactive profiles as PENDING from recent invitation activity, and
backfills the new timestamp columns.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-30 09:19:42 +02:00
Sacha Al Himdani
cd6c46212a Add log export for audit logs and SCIM events
Route audit-log and SCIM-event exports through export_jobs with typed
arguments, an iam BuildAndUploadExport/SendExportEmail implementation,
and a concurrent export-job worker with stale recovery. Stream JSONL via
page.WalkAll into S3, and expose the request flow on console, connect,
MCP, and CLI.

Co-authored-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-29 18:57:31 +02:00
Cursor Agent
255bea4738 Make risk delete prove document mapping cleanup
Removing junction rows before deleteRisk hid missing soft-delete
cleanup in e2e. Assert deleteRisk succeeds only after deleteDocument
clears mappings, and document links must be removed first.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-29 18:00:34 +02:00
Cursor Agent
f451e94c0b Clear document mappings on soft delete
Document soft delete cleared generated-document references but left
control, risk, and measure junction rows pointing at deleted documents.
That blocked risk deletion and made unlinkRisk fail when the document
was already gone.

Remove entity mappings in SoftDelete and BulkSoftDelete, drop mappings
before deleting a risk, tolerate missing documents when unlinking, and
backfill orphaned junction rows for soft-deleted documents.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-29 18:00:32 +02:00
Ludovic Vielle
ad615a47a0 Show posture values and report history
Pass/fail was the main device UI signal, but operators need
the agent's observed value. Expose a formatted value per
check, show current postures on the device page, and replace
the Postures tab with paginated report history grouped by
agent push time. Status stays in the model for later rulesets.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-29 16:27:23 +02:00
Cursor Agent
9e9272b4ac Add sort fields for audit engagement dates
Expose AUDIT_START_DATE and AUDIT_END_DATE on AuditOrderField
so list queries can paginate like valid_from and valid_until.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:34 +02:00
Cursor Agent
c7b1562d26 Store audit engagement dates as DATE columns
Match valid_from and valid_until on audits and the console
date-only inputs for audit start and end.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:34 +02:00
Cursor Agent
f8f5152566 Use timestamptz for audit engagement dates
Store audit_start_date and audit_end_date as TIMESTAMP WITH
TIME ZONE. Alphabetize n8n audit field collections to satisfy
n8n-node lint.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:33 +02:00
Cursor Agent
fe215efbd1 Add audit start and end dates to audits
ISO audits often span a window distinct from certificate validity.
Store optional audit_start_date and audit_end_date on the audit
record and expose them through GraphQL, MCP, CLI, n8n, and console.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-28 17:27:33 +02:00
Émile Ré
b64f101ab9 Drop resend verification email cooldown
Resend only runs after an explicit form submit, so a per-address
cooldown is unnecessary overhead compared with forgot-password.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-28 15:11:06 +02:00
Émile Ré
82a62005f8 Harden email verification resend against abuse
Add a per-address confirmation-email cooldown and disable the
resend/forgot-password submit buttons while the mutation is in
flight so callers cannot flood the mail queue or double-submit.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-28 15:11:06 +02:00
Bryan Frimin
636dd172e8 Fix wsl_v5 spacing in MergeByCampaignID
Add blank lines around the dedup-and-connector-ID block to satisfy
the wsl_v5 linter (blank line after an if-block, above an assignment
following a block, and above an if with no shared variables in the
preceding line).

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-27 18:56:25 +02:00
Bryan Frimin
b43e66ffa4 Remove restated doc comment on MergeByCampaignID
The comment repeated what the function signature and body already
say plainly.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-27 18:56:25 +02:00
Bryan Frimin
68a64647bf Batch-load and validate campaign sources before merging
MergeByCampaignID joined access_review_sources directly from coredata
to resolve live sources, so unrecognized or out-of-scope IDs were
silently dropped instead of erroring, and in the worst case (every ID
invalid) the NOT MATCHED BY SOURCE clause deleted every existing
campaign source. The syncCampaignSources ErrResourceNotFound check
was therefore unreachable dead code.

Add AccessReviewSources.LoadByIDs, matching the existing scoped
LoadByIDs pattern (id = ANY(@ids) plus a resolved-count check), and
have CreateCampaign, AddCampaignSource, and syncCampaignSources
resolve and validate sources up front. MergeByCampaignID now takes
the already-loaded sources and builds its desired-state CTE from an
unnest() of their values instead of joining access_review_sources,
keeping the merge inside the campaign-source entity boundary.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-27 18:56:24 +02:00
Émile Ré
15b51640d3 Fix geoloc country ip block import script
Signed-off-by: Émile Ré <emile@probo.com>
2026-07-27 18:21:57 +02:00
Cursor Agent
3455784f52 Remove unused uniqueGIDs helper
MergeByCampaignID already dedupes with gid.NewSet.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
b5574764cf Dedupe campaign source IDs with gid.NewSet
Remove local uniqueGIDs helper in favor of the existing gid set
utility used elsewhere in coredata.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
3b3d7002a9 Drop pre-merge access source count check
Scoped rows in the MERGE desired_sources CTE already limit
snapshots to sources visible under the scoper.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
4d154651a8 Use one path for campaign source merge
Remove early return on empty source IDs; count and MERGE always
run so clearing sources uses NOT MATCHED BY SOURCE delete.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
447bdb88e5 Early-return empty campaign source merge
Delete scoped snapshots in one statement when the desired source
set is empty; validate and merge only when IDs are present.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:37 +00:00
Cursor Agent
abbb1ee875 Merge campaign scope sources in one SQL statement
Add AccessReviewCampaignSources.MergeByCampaignID and use it from
syncCampaignSources instead of per-row load, upsert, and delete.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-07-27 15:56:36 +00:00
Sacha Al Himdani
d61ec8dd65 Add search and status/role/type filters to People
Makes it practical to find people in larger orgs across GraphQL, MCP, CLI, and n8n, with page size raised to 100.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-27 17:20:18 +02:00
Steven4Hooisma
e62e6cce43 Add UpCloud integration with account listing and details retrieval
Signed-off-by: Steven4Hooisma <112615049+Steven4Hooisma@users.noreply.github.com>
2026-07-27 11:08:24 +02:00
Aurélien Sibiril
20502fc0be Use the MIT license header on the new connector files
The four connectors landed with the ISC header. contrib/claude/license.md
mandates MIT for every source file, and these were the only ISC files
left in pkg/accessreview/drivers and pkg/connector/provider — the two
"Make license declarations consistently MIT" commits had already cleared
both packages, so this re-opened a closed cleanup.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 09:22:05 +02:00
Aurélien Sibiril
60628645ae Add Google Analytics, Dotfile, Segment and Square access-review connectors
Two OAuth2 and two API-key connectors:

- Google Analytics (GA4): OAuth2 with both analytics.readonly and
  analytics.manage.users.readonly (readonly alone 403s on the accounts
  list); v1alpha accessBindings enumerated at account and property level
  and merged by email; manual account picker (Pattern 1) with a
  per-connection probe and name resolver; distinct from Google Workspace.
- Dotfile: API key in the X-DOTFILE-API-KEY header (Pattern 3); GET
  /v1/users (owner/admin, suspended_at) with a static probe.
- Segment (Twilio): Public API token as Bearer with a required Region
  setting (US or EU) mapped to the regional host; GET /users plus per-user
  GET /users/{id} for roles and /invites for pending members; per-connection
  BuildProbeURL.
- Square: OAuth2 (EMPLOYEES_READ) or a personal access token (Pattern 3);
  POST /v2/team-members/search returns email/status/is_owner directly, so no
  role resolution; custom probe and name resolver.

Google Analytics and Square are confidential OAuth clients, wired into the
bootstrap OAuth provider list and .env.example. Segment carries a required
extra setting, so the console add-source dialog maps region onto its
segmentRegion API-key input; without that mapping the value is silently
dropped and the create is rejected.

Cassette-backed driver tests plus unit tests for the Segment probe URL and
the bootstrap OAuth provider list.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 09:22:04 +02:00
Ludovic Vielle
1f79453386 Add device data model and ITAM service
Introduce device, posture, and enrollment-token entities with ITAM
service policies for agent-managed fleet inventory.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-24 15:08:21 +02:00
Sacha Al Himdani
bcd05a2e55 Reject empty SAML NameIDs on login
Empty NameID values were stored as '' and occupied the
unique saml_subject index, causing duplicate-key failures
on later logins. Reject blank NameIDs during assertion
validation, return a clear error when a NameID is already
linked to another account, and stop returning internal
errors from the SAML consume endpoint.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-24 14:03:30 +02:00
Bryan Frimin
0b577cda40 Fix global rate limit ACME
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-22 17:53:20 +02:00
Aurélien Sibiril
a0ead155db Trim access-review connector comments
Cut verbose inline comments across the access-review connector changes:
the mechanical fact stays at the read site, while incident backstory
("millions of error logs in prod") and provider-specific rationale (why
Clerk reviews the wrong population) move to the commit history where they
belong. Also tighten a loose "a 4xx" to "an auth/not-found 4xx" so the
terminal-classification contract is not overstated. No behavior change.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
f711e9d816 Reset access source name sync on connection change
Once the source-name worker sets name_synced_at, nothing cleared it,
so a source that hit a terminal failure (Brex 403, wrong Sentry org
slug) kept its generic name forever — even after the user reconnected
with the right scope or picked the correct org, contradicting the
Brex guidance to reconnect.

Clear name_synced_at whenever the connection changes: on UpdateSource
when a connector is (re)set, inside ConfigureAccessReviewSource when
the org is (re)selected, and on the OAuth reconnect path via the new
ResetSourceNameSyncForConnector service method. The worker then
re-claims the row and re-resolves the display name.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Aurélien Sibiril
60d373e5c4 Disable Clerk access-review connector
Clerk's Backend API secret key only exposes the customer's application
end-users (GET /v1/users) -- the people who sign into the customer's
product -- not the Clerk workspace/dashboard team who administer the
platform. Those admins have no Backend API and are managed only in the
Clerk Dashboard UI. So an access-review campaign against Clerk reviews
the wrong population entirely (verified in prod: one source paginating
16K+ end-users), which is a compliance defect, not just the pagination
timeouts it also caused.

Unregister Clerk from the builtin provider registry and drop its driver,
provider registration, tests, and cassette so it can no longer be added
or fetched. Keep the ConnectorProviderClerk enum value, its IsValid
membership, the GraphQL enum value, and the frontend logo so existing
stored CLERK connectors still validate, serialize, and display (users
can still see and remove them). Drop it from ConnectorProviders so the
registry stays in sync with the registered set.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-22 17:27:55 +02:00
Bryan Frimin
81b7ee5fad Close cert provisioning correctness gaps
Several race and validity gaps could leave certificate provisioning
stuck, unusable, or noisy:

- Accept the HTTP-01 challenge only after the key authorization is
  committed, so the CA cannot hit the token before this instance can
  serve it and invalidate the order.
- Persist challenge metadata under a blocking write-back lock; a row
  merely locked by a competing transaction no longer silently drops the
  accepted order.
- Abandon a recovered VALID order and restart instead of issuing it
  with a freshly generated key that cannot match the existing cert.
- Exclude rate-limited rows from the ten-minute stale reset so the
  resumable order survives the ACME cooldown.
- Size the provisioning poll lease to exceed the max processing window
  so a released claim lock cannot let another worker process the same
  row concurrently.
- Parse Retry-After as unsigned seconds and clamp overflow so malformed
  values fall back to the default cooldown instead of disabling it.
- Normalize the acme_errors problem_type label to the RFC 8555 set to
  bound Prometheus cardinality.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-22 13:20:53 +02:00
Bryan Frimin
121d4dcf93 Fix certmanager provisioning retry and metrics
Address several provisioning defects that either stalled the retry
budget or crashed the process:

- Classify CAA resolver/transport failures apart from a real CAA policy
  denial. Both shared the "caa records" wording, so a transient resolver
  error was persisted as customer misconfiguration and retried forever
  without consuming the retry budget. A new ErrCAANotPermitted sentinel
  now marks the genuine misconfiguration; other CAA errors are treated
  as ordinary transient failures.

- Honor an explicit Retry-After: 0 (or a past date) as permission for an
  immediate retry instead of promoting it to the one-hour default
  cooldown. acme.RateLimit collapses zero, invalid, and absent headers
  to a zero duration, so the header is now parsed directly to tell an
  explicit zero apart from a missing one.

- Reuse already-registered Prometheus collectors when a second
  ACMEService shares a registerer. The fixed-name collectors were
  MustRegistered, so a duplicate registration panicked the process.

- Persist provisioning failures on a context detached from the process
  tick deadline. A timed-out attempt reached persistFailure with an
  expired context, so the write-back failed and the retry budget never
  advanced, leaving the certificate indefinitely retriable.

- Use pgx.StrictNamedArgs in the certificate FOR UPDATE loaders to match
  the coredata SQL contract.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-22 13:20:52 +02:00
Bryan Frimin
9724a2ce50 Harden cert provisioning failure and write-back
Rate limits no longer inflate ssl_retry_count into an instant FAILED
path. Centralize outcomes in decideProvisioningOutcome, keep ACME
order state on transient and rate-limit errors, bound each Process
tick with a timeout, and block on FOR UPDATE when persisting a
freshly issued certificate.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-22 13:20:52 +02:00
Émile Ré
0d832508c9 Replace portal title with entity name
Store a short entity name instead of the full home
heading so orgs can brand portals for sub-entities.
Restore hero i18n composition and keep the English
document title composed from the entity name.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-22 13:08:02 +02:00
Émile Ré
418bb5a8f8 Address review feedback on portal i18n
Swallow locale mutation rejections after the toast, close the
mobile drawer on locale change, escape SEO paths, share the
IAM locale list with SEO, and finish dropping /trust leftovers.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-21 18:43:50 +02:00
Émile Ré
c223873e96 Put locale in compliance portal URLs
Path-segment locales make each language crawlable with self
canonical and hreflang, while identity.locale persists an
explicit choice without client storage or cookie banners.

Signed-off-by: Émile Ré <emile@probo.com>
2026-07-21 18:43:48 +02:00
Bryan Frimin
63dffe801e Store full portal titles as home headings
The UI composed "Compliance at {{name}}." via i18n while the
DB only held the org name. Persist the full heading, backfill
existing rows, and drop the unused heroTitle locale keys.

Also use clientip.Extract for NDA signing events and default
the portal base domain to probopage.localhost for local runs.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:29 +02:00
Bryan Frimin
a3d6c120d7 Mint certificate GIDs with entity type 106
Certificate backfill migrations used entity type 104, which belongs to
commitment groups. Use 106 so migrated certificate IDs decode correctly.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:28 +02:00
Bryan Frimin
4bf3d4df79 Rename trust center coredata types
Align entity types, order fields, and visibility
enums with the Compliance Portal product name so
the data layer matches the rest of the rename.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:23 +02:00