Expose a non-destructive re-trigger that re-arms tracker policy
generation for a banner that already has a published version, so the
tracker-policy worker regenerates the document after iterating on the
generator. RegenerateTrackerPolicy returns a conflict when nothing has
been published yet.
Wire it across all API surfaces per the api-surface rule: the console
GraphQL mutation and resolver, the MCP tool, the prb cookie-banner
regenerate-policy command, and the n8n operation, with console e2e
coverage for the success and no-published-version paths.
Signed-off-by: Émile Ré <emile@probo.com>
Expose the existing tracker_patterns.common_tracker_pattern_id foreign
key on the TrackerPattern type so it is possible to tell whether a
pattern is linked to the global common-tracker catalog (its description
likely came from the seed or the mapping/enrichment agents) or has no
link (added manually or inherited). This is a read-only debugging aid
for agent-generated descriptions; no migration or write path changes.
The field is added in sync across all four API surfaces (GraphQL, MCP,
CLI, n8n) plus the console UI, and covered by e2e assertions for both
the linked and unlinked cases.
Signed-off-by: Émile Ré <emile@probo.com>
Add a SigNoz connector so its organization members can be pulled into
access-review campaign snapshots. SigNoz authenticates with a
SIGNOZ-API-KEY admin service-account key and a customer-supplied base
URL (a SigNoz Cloud region/tenant host or a self-hosted instance).
The driver lists users via GET /api/v1/user, which returns the role
(ADMIN/EDITOR/VIEWER) inline so admin detection works in a single call,
and maps the SigNoz user status (active / pending_invite / deleted) to
the account active flag. The name resolver reads the organization
display name from GET /api/v2/orgs/me to title the access source.
Wire the provider through the coredata enum and settings, the
connector-provider registry (driver and name-resolver factories), the
console API-key input schema and validation, the access-review source
label, and the SigNoz brand logo.
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Introduce RiskAssessmentBoundary as a first-class, self-nesting entity that
groups nodes within a risk assessment scope, and thread it through every
surface.
- coredata: new risk_assessment_boundaries table + migration, boundary_id on
nodes, self-referential parent_boundary_id, entity type registration
- riskmanagement: boundary CRUD service methods, boundary_id wiring on node
create/update, scope-membership and self-parent validation, nested-subgraph
Mermaid rendering
- IAM: core:risk-assessment-boundary:{get,list,create,update,delete} actions
and viewer/auditor read policies
- console GraphQL: RiskAssessmentBoundary type, connection, order enum, CRUD
mutations, boundaries field on scope, boundaryId on nodes
- CLI: risk-assessment boundary command group and --boundary-id on nodes
- MCP: boundary tools and boundary_id on node tools
- n8n: boundary operations and boundary fields on node operations
- console UI: boundary list/create/edit, boundary selector on nodes, diagram
refetch on boundary changes
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Users were missing the expiry notice and hitting link errors. Replace the
small gray footer text with a prominent yellow callout box in the HTML
email, and prefix the plain-text version with IMPORTANT.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Bryan Frimin <bryan@probo.com>
Add the Zendesk brand mark under ThirdParties and wire it into the
logo map. Because the customer subdomain is free-form (not a fixed
region list), the connect flow opens a dialog with a subdomain text
input that is sanitized and passed as ?site= to the OAuth initiate
endpoint, mirroring the Datadog multi-site dialog.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Add the Okta brand mark to the ThirdParties atoms (export, logo map,
and ThirdPartyLogo wiring) and map the generic "domain" extra setting
to the oktaDomain field in the add-access-source dialog so the
API-key connect flow submits the org domain.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
GET /v1/users returns a bare JSON array, not a {"data":[...]} envelope
(total_count is a separate endpoint), so decode directly into a slice.
The previous envelope-first decode errored on the array and never
reached the fallback, so ListAccounts failed against the real API.
Treat deprovisioned users as inactive. Add the missing Clerk
third-party logo and its wiring, and document why the registration
sets no probe URL or name resolver.
Record the driver-test cassette against a live Clerk development
instance, scrubbed of PII and instance identifiers (emails to
example.com, image_url payloads and CF transport headers dropped).
A locked account verifies the inactive path.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
- Add SendGrid third-party logo and wire it into ThirdPartyLogo
- Add SendGrid name resolver (account company name, graceful fallback)
- Fix MFA detection: full-access teammates carry both 2fa_exempt and
2fa_required, so report Unknown unless exactly one is present
- Re-record the driver cassette against the live API
- Use a random time suffix for the migration filename
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Fold POSTHOG_SELF_HOSTED into POSTHOG: one provider now covers Cloud (OAuth + region-pinned API key) and self-hosted (API key + instance URL), since both already share the driver, name resolver, and PostHogConnectorSettings{BaseURL}. The API-key form picks a deployment (Cloud US/EU or self-hosted URL); the resolver requires exactly one of region/instanceUrl.
Drop the POSTHOG_SELF_HOSTED enum value, registration, migration, and logo mapping. Extract the deployment selector into a dedicated PostHogDeploymentField component. Point the driver tests at us.posthog.com instead of the legacy app.posthog.com host.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Show a US/EU region dropdown for the PostHog Cloud API-key form and
an instance-URL field for Self-Hosted, map both to the connector inputs,
and register the PostHog logo for the self-hosted provider.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The themed banner and headless cookie list exposed only the tracker
name, description, and duration, so trackers that share a name but
differ in technology were indistinguishable to visitors.
Add the tracker type to the headless CookieItem model and render it as a
labeled "Type: <value>" detail under the tracker name, with an explicit
aria-label for assistive technology. Tracker type names are Web platform
API names (Cookie, Local storage, IndexedDB, ...), so they are kept
canonical while only the surrounding label is localized.
Signed-off-by: Émile Ré <emile@probo.com>
Queue vetting on third_parties with PENDING, PROCESSING,
COMPLETED, and FAILED states. Expose enqueue and status through
GraphQL, MCP, CLI, and n8n, validate vet requests, tune the
worker via config, and poll the detail page while vetting runs.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
The coredata CookieSource enum and the ingestion path both support an
HTTP source, but the GraphQL CookieSource enum never declared it. The
generated marshaler is a plain map lookup with no fallback, so an HTTP
value missed the map and serialized to an empty string. The console UI
treats that empty string as falsy and rendered no source badge at all,
making HTTP-sourced trackers look sourceless.
Add the HTTP member to the GraphQL enum so the value round-trips, and
fold the duplicated tracker-type and tracker-source badge helpers from
three components into a shared @probo/helpers module, adding an explicit
HTTP label while consolidating.
Signed-off-by: Émile Ré <emile@probo.com>
Allow vendor country selections to use a Global region alongside
existing country and EU values. The new value is accepted by backend
country-code validation, exposed through GraphQL and MCP schemas, and
shown in the shared country picker label set.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Restore RemoveUser as a hard delete operation and surface dependency\nconflicts with a dedicated IAM error.\n\nAdd a new ArchiveUser flow that deactivates profiles while keeping the\nmember in the organization, then expose both actions across Connect, MCP,\nCLI, n8n, console UI, and e2e coverage.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Reorder the user operation list so the n8n lint rule requiring\nalphabetized option labels passes after introducing the Archive\noperation.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Align user-removal UX and API surface semantics with the new archive\nbehavior for manually managed users.\n\nFrontend copy and actions now use archive wording, and list rows are\nrefetched after the mutation so archived users reappear as inactive.\n\nMCP removeUser now documents and returns archived_user_id, n8n labels\nand response mapping now use archive semantics, and the CLI gains a\nuser archive command backed by the same mutation.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Add a deferred PostHog wiring under examples/cookie-banner-react that
boots posthog.init() inside the probo-ready handler and derives
cookieless_mode and opt_out_capturing_by_default from the consent
snapshot for the category flagged with posthog_consent.
Driving the init args off the snapshot rather than consent_mode plugs
two cases the simpler "consent_mode alone" rule got wrong:
* OPT_OUT regulation, returning rejector: init would have booted in
on_reject + capture-on, fired a $pageview synchronously, and only
then called opt_out_capturing(). That single captured pageview
(and the posthog cookie) leaked on every page load.
* OPT_IN regulation, returning acceptor: init would have forced
"always" + opt-out, costing the visitor cookies and a one-tick
capture delay even though they had already consented.
The snapshot already encodes the regulation default
(buildDefaultConsentData on the cookie-banner client returns true for
non-necessary categories under OPT_OUT and false under OPT_IN) and any
persisted answer from a prior visit, so a single boolean drives both
init args.
Re-export the public domain types (BannerConfig, Category, Regulation,
ConsentAction, ConsentRecord, CookieItem, VisitorConsent) from
@probo/cookie-banner so the example can type the probo-ready event
detail without duck-typing it.
Adopt the PUBLIC_ env prefix in Vite so the example reads the same env
var names (PUBLIC_COOKIE_BANNER_ID, PUBLIC_COOKIE_BANNER_API_BASE_URL,
PUBLIC_POSTHOG_API_KEY) already used on getprobo.com, and add a
matching .env.example.
Ignore *.tsbuildinfo at the repo root; TypeScript's incremental cache
is machine-local and does not belong in the tree.
Signed-off-by: Émile Ré <emile@probo.com>
When fetchJSON failed, load() unconditionally overwrote this.observer
with a fresh MutationObserver, leaving any previous one observing the
DOM. Repeated load() calls that hit the catch branch leaked one
observer per call. Mirror the disconnect-first pattern already used in
activate().
Signed-off-by: Émile Ré <emile@probo.com>
GCM now creates dataLayer and denies all consent types before the
config fetch, closing the gap where gtag could track freely during
the async config load. PostHog integration is removed because
script blocking via data-cookie-consent is the correct mechanism
for vendors that don't support a queue/replay pattern.
Signed-off-by: Émile Ré <emile@probo.com>
Add a many-to-many relationship between measures and third parties,
surfaced as a measures tab on the third party detail page and a third
parties tab on the measure detail page. Each side gets a paginated
list with a link/unlink dialog.
Also remove the right-hand drawer on the measure detail page and
expose the state as a badge in the page header, mirroring how the
compliance page surfaces its active flag.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Introduce a self-referential many-to-many relation table so a
third party can have child third parties. Each relation is
directional (parent to child); both directions can coexist as
independent rows.
Add a first_level boolean on third_parties (default true) with
a filter on the list page that defaults to showing only
first-level third parties.
Frontend adds a "Third Parties" tab on the detail page where
users can link existing third parties or create new ones from
the common third party catalog (created as non-first-level).
The list page gets a First Level/All toggle filter.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
STACK_URL_RE only matched `https?://` URLs, so extension URLs
(`chrome-extension://`, `moz-extension://`, `safari-web-extension://`)
never reached the EXTENSION_URL_RE classifier. The `fromExtension`
flag was effectively always false and extension-originated cookie
and storage activity was misattributed as `script`.
Broaden STACK_URL_RE to also match the three extension schemes so
the existing classifier branch fires and sets `fromExtension = true`
before the URL parsing path runs.
Signed-off-by: Émile Ré <emile@probo.com>
The previous cleanup deleted every isExtensionCaller() site, including
the one in cookie/storage detectors that did fire reliably for the
residual case: page-world extensions (MV3 main world, userscripts with
@grant none) whose stack contains a chrome-/moz-/safari-web-extension
frame at the synchronous write. Recover that signal for free by
returning fromExtension from getInitiatorURL (it already walks the
stack and discards extension frames via continue), and have the cookie
and storage detectors report source: "extension" instead of "script"
when the flag is set.
End-to-end plumbing reuses the existing source column: extend the
cookie_source Postgres enum with EXTENSION, add the CookieSourceExtension
constant with a doc block describing each bucket's actual semantics,
add the handler.go switch cases, expose EXTENSION on the GraphQL and
MCP CookieSource enums, and add the Extension option to the console
source filter.
Update bestSource in the pattern analysis worker so a glob merging
only extension-attributed exact patterns is no longer silently rolled
up to PRE_EXISTING. New precedence is SCRIPT > EXTENSION > PRE_EXISTING,
matching the upsert SQL's "page-script wins" rule and the asymmetric
signal strength of each bucket.
Out of scope: any behavioural use of EXTENSION (auto-exclusion,
denylist classification, dashboard surfacing) -- that belongs in the
follow-up backend denylist plan.
Signed-off-by: Émile Ré <emile@probo.com>
The synchronous Storage/Document/Element/fetch/XHR/sendBeacon wraps
and the resource-detector attribution machinery relied on
isExtensionCaller() finding a chrome-extension:// frame in the JS
stack. For Chromium/Edge/Safari MV3 isolated-world content scripts
-- the dominant case -- those wraps live in the page realm while
the extension uses its own copy of every prototype we hook, so the
check never fires and the marking never runs.
Backend denylisting (planned) covers the same cases more cheaply,
retroactively, and across all tenants, so the elaborate frontend
plumbing no longer earns its complexity. Keep only the parts that
backend classification cannot replace: isExtensionContext() (SDK
loaded inside an extension page) and the http/https-only filter
in processResource (drops chrome-extension:// URLs surfaced via
PerformanceObserver). resource-detector.ts shrinks from ~920 to
~225 lines.
Signed-off-by: Émile Ré <emile@probo.com>
handleAttributeMutation returned early when resourceTypeForElement
yielded null, which happens for `<link href>` whenever `rel` is not yet
"stylesheet". An extension that called `link.setAttribute("href", ...)`
before setting `rel` therefore skipped extensionElements and
extensionUrls marking; when the rel was filled in later and the browser
fetched the stylesheet, PerformanceObserver -- whose stack carries no
extension frame -- reported it as a page tracker.
Check isExtensionCaller() before classification and tag the element and
URL whenever the element type can ever initiate a load via the given
attribute, using a new couldLoadResource helper. The page-caller path
still uses the strict resourceTypeForElement so non-stylesheet `<link>`
writes do not generate spurious reports.
Signed-off-by: Émile Ré <emile@probo.com>