Three follow-ups from review of the registry consolidation:
- Build Vercel's authorization URL with net/url instead of a
hand-rolled "{integration_slug}" placeholder resolved by
strings.ReplaceAll. The slug is escaped via url.PathEscape in a
per-provider Registration.BuildAuthURL closure, and the unused
AuthURLParams plumbing on Registration and OAuth2Connector is
removed (OAuth2Connector now carries a typed IntegrationSlug).
- Drop the SettingsInput union type and the per-provider
MarshalSettings closures. The create resolvers now build the typed
coredata.*ConnectorSettings directly from the gqlgen input, the
same way the OAuth callback path already does, so there is no
shared catch-all DTO and no stringly-typed boundary.
- Restore ConnectorProviders() to a plain ordered slice literal; the
intermediate map + slices.Sort added nondeterminism and a sort for
no benefit.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The console previously dispatched per-provider logic through a fan of
init()-side-effect maps (driver names, OAuth2 metadata, probe URLs,
display names, settings switches), spread across pkg/connector,
pkg/accessreview/drivers and the console v1 resolvers. Adding a new
provider required edits in every one of those places and a corresponding
switch arm in CreateConnectorRequest. The same per-provider knowledge
also leaked into Helm templates as hand-rolled environment-variable
blocks per connector.
This commit collapses the dispatch surface into a single typed
*provider.Registry. The registry is constructed once by
NewBuiltinRegistry at probod startup and threaded as an explicit
dependency into every consumer (accessreview service, console v1
resolver, OAuth2 wiring). There is no package-level state. Each
provider lives in one file under pkg/connector/provider/ that exposes
a private xxxRegistration() *Registration constructor; NewBuiltinRegistry
enumerates them.
CreateConnectorRequest loses its per-provider settings fields and
takes a single RawSettings json.RawMessage produced by the
per-provider MarshalSettings closure. The 1Password SCIM bridge URL
is validated at create time (http(s) scheme + non-empty host) so a
malformed value fails fast at the resolver boundary. The Helm chart
gains probo.connectorEnv and probo.connectorSecretEntries templates
so adding a connector requires zero Helm changes. Access-review name
resolution moves into the same Registration value to keep one
authoritative dispatch table.
Tests cover every Registration (DisplayName, NewDriver wired),
Register error paths (nil, empty Provider, empty DisplayName,
duplicate), All / ProviderDisplayName / ProviderOAuth2Scopes /
ProbeURL hit and miss paths, the ApplyOAuth2Defaults templating and
PKCE branches, and ConnectorSettings[T] round-trip plus malformed-JSON
error path. The pre-refactor ApplyProviderDefaults test in
pkg/connector is replaced by the equivalent in
pkg/connector/provider.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Every PromoteSource caller already loaded the tracker pattern under
the same transaction, so a dedicated single-column UPDATE only
duplicated machinery and forced callers to learn a second mutation
verb. Add `source = @source` to Update's SET clause, mutate
Source/UpdatedAt on the receiver, and call Update at the three
promotion sites (worker merge loop, worker adoption loop, and
reportDetectedTracker). The shouldPromoteSource gate still ranks the
candidate against the loaded value; Update is now the single write
path that can advance source, with a doc comment spelling out the
load-first contract.
Re-cast the coredata tests around Update: WritesSource pins the
round-trip from receiver to DB, NotFoundForMissingRow preserves the
ErrResourceNotFound contract callers rely on. The old
OnlyTouchesSourceAndUpdatedAt test was a property of the narrow
PromoteSource UPDATE and no longer applies — Update intentionally
rewrites the full editable column set from the receiver.
Signed-off-by: Émile Ré <emile@probo.com>
The pattern-analysis worker dropped two signals on every run. When
InsertIfNotExists hit a pre-existing glob, the computed bestSource
was discarded by the LoadByBannerIDTypeAndPattern fallback, so the
SCRIPT > EXTENSION > PRE_EXISTING precedence advertised on
bestSource was only ever enforced at first insert. Subsequent
batches with stronger sources could not promote the glob, even
though the page-script-wins rule already lives in detected_trackers
at the row level.
Separately, adoptUncategorisedPatterns returned an adopted bool
that the worker discarded; the function moves detected trackers
from uncategorised exact patterns into categorised globs, which is
a real consent transition, but no draft banner version was created
on adoption-only runs.
Add a focused TrackerPattern.PromoteSource that only updates the
source and updated_at columns. Express the precedence as a pure-Go
shouldPromoteSource helper alongside bestSource so the rule is
unit-testable without a database. The worker now calls
InsertIfNotExists, then on conflict loads, skips when the slot is
held by an exact pattern or a user-recategorised glob, and only
calls PromoteSource when the candidate source ranks above the
existing one.
The skip branch is now documented: adoptUncategorisedPatterns is
the safety net that re-homes uncategorised exacts into the existing
glob via globMatch. Capture its adopted return value and use it
(instead of the previous over-eager consentChanged flag) to gate
ensureDraftVersionForBanner. Merging exacts into a glob in their
own category never changes visitor consent, so the prior flag
produced redundant draft versions on every non-uncategorised merge.
Cover the new pieces with three test layers: pure-unit cases for
shouldPromoteSource (precedence matrix including HTTP/nil collapse
and equal-rank no-write), DB-backed tests for PromoteSource (touch
only source + updated_at, ErrResourceNotFound for missing rows),
and end-to-end worker tests for source promotion on an existing
glob, draft-on-adoption, and the merge-only no-draft case.
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>
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>
Audited pkg/server/api/console/v1 for resolvers that touched tenant
data without calling r.authorize, batchAuthorize, or Permission. Closed
every gap so every data-bearing field goes through IAM (and produces an
audit log entry when an organization_id is present).
* High-severity reads now authorize: accessSourceResolver.Connector and
ConnectionStatus, controlResolver.Regulatory/Contractual/RiskAssessment,
electronicSignatureResolver.CertificateFileURL/Events,
commonThirdPartyResolver.LogoURL, and the proper
accessSourceResolver/accessReviewCampaignResolver/auditLogEntryResolver
Organization resolvers (authorize + dataloader load, fixing the latent
empty-name bug from the previous force-resolver no-op implementations).
* TotalCount/DetectedCount aggregates now authorize the matching list
action across access review, audit log, statement of applicability,
detected tracker, tracker pattern, and tracker resource connections.
* queryResolver.CommonThirdParties authorizes against the principal's
identity via the new identity-scoped CommonThirdPartyCatalogPolicy.
* Add ActionCommonThirdPartyGet/List, ActionElectronicSignatureGet probo
action constants; wire ActionElectronicSignatureGet into ViewerPolicy
and AuditorPolicy.
* Implement AuthorizationAttributes on CommonThirdParty (no org) and
ElectronicSignature (organization_id) so the authorizer can resolve
attributes for the new actions.
* Delete the dead "type AccessReview" GraphQL type (no Go constructor,
no frontend reference) and drop its orphan resolver bundle.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Change AuthorizationAttributer.AuthorizationAttributes to take a slice
of resource ids and return policy.AttributesByID, so a single SQL
round-trip can load condition attributes for a whole batch. All
coredata implementations are migrated to a single
`WHERE id = ANY(@resource_ids::text[])` query that returns only the
rows it finds.
Authorizer gains:
- AuthorizeBatch — all-or-nothing across a homogeneous (same entity
type, same organization) resource set; rejects mixed entity types,
mixed organizations, and empty batches with structured errors.
- AuthorizeMulti — heterogeneous evaluation that returns one error
per item and writes audit log entries in a single bulk insert.
The single-resource Authorize is rewired to delegate to AuthorizeBatch
so all paths share the same condition evaluation and audit logging.
recordAuditLog is split into buildAuditLogEntry plus a batch insert.
Tests cover the new batch and multi paths, mixed/empty/unsupported
resource cases, audit log batching, and dry-run behaviour.
Signed-off-by: Bryan Frimin <bryan@probo.com>
The badge on a document version showed signatures filtered by
activeContract: true, while the signatures tab fetched signatures with
no filter and listed people filtered by contractEnded: false and
state: ACTIVE. The two views disagreed both when a signer's contract had
ended and when a signer was deactivated while still under contract.
Add a state: ProfileState field to DocumentVersionSignatureFilter
alongside the existing activeContract filter, so the signature query
can mirror the same predicates as the people query. Pass
{ activeContract: true, state: ACTIVE } from the badge, the document
list item, and the signatures tab fragment. The same filter is now
evaluated on both the count and the list.
Threaded through the console and MCP resolvers, the MCP spec, and the
n8n getAllSignatures operation.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Replace duplicated LoadConsentCategoriesByCookieBannerID,
CountConsentCategoriesByCookieBannerID, and
LoadAllConsentCategoriesByCookieBannerID with a single
CookieCategoryFilter in pkg/coredata. The filter uses the
standard CASE WHEN idiom to optionally exclude a kind,
eliminating branching in the service layer.
Signed-off-by: Émile Ré <emile@probo.com>
The third-party list template emitted a text node for every optional
field even when the value was empty, producing `{"text":""}` nodes that
violate the ProseMirror schema and make Tiptap refuse to render the
document with "Empty text nodes are not allowed".
Add a `default` template helper and substitute "—" for empty values in
third_party_list.json.tmpl, and add a migration that rewrites existing
document_versions.content to drop any empty text nodes (per-row safe,
preserves marks/attrs/ordering, leaves updated_at untouched).
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Store the per-signature email subject as text on the electronic_signatures
row at creation time, mirroring the consent_text pattern. The document
approval service sets "Your approved <Title> - Certificate of Completion";
other callers default to "Your signed <Name> - Certificate of Completion".
The certificate worker uses signature.email_subject as the email subject,
falling back to the default format when the column is empty.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Add missing blank lines around if-block boundaries in two files
to satisfy wsl_v5, and make lint-go and lint-js fail the build on
pull requests (not only on push to main) by always running the
strict lint and using reviewdog purely for inline annotations.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Deleting a membership profile referenced by other tables (owner,
approver, assignee, etc.) surfaced as a generic Internal error.
Detect the Postgres FK violation (23503) in the coredata Delete,
return ErrResourceInUse, and map it to CONFLICT in the GraphQL
and MCP resolvers so the client sees an actionable error.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Apply five style rules: convert iota string enums to typed
string constants, replace errors.As with errors.AsType,
merge three-group imports into two groups, fix multiline
parameter/argument formatting, and replace fmt.Sprintf URL
construction with net/url.
Signed-off-by: Émile Ré <emile@probo.com>
Remove dead 23505 checks on single-GID primary keys (oauth2_consent,
risk_assessment, risk_assessment_scenario, risk_assessment_scope).
Add missing constraints to membership_profile and
statement_of_applicability. Document composite-PK vs GID-PK rule in
cursor rules and contrib guide.
Signed-off-by: Émile Ré <emile@probo.com>
Delete methods no longer check RowsAffected — deletes are
idempotent. PgError handlers now check both error code and
constraint name to avoid misattributing violations. Also
migrated remaining errors.As patterns to errors.AsType.
Signed-off-by: Émile Ré <emile@probo.com>
Each scope card now shows a flowchart of its nodes, processes, and
threats, with a distinct shape per type: stadium for entities,
hexagon for boundaries, rectangle for assets, cylinder for data, and
a red hexagon for threats attached via dashed edges to their process
target. The Mermaid source is built on the backend and exposed as a
new `mermaid` field on RiskAssessmentScope; the frontend just renders
it via @probo/ui's MermaidDiagram and shows a copy button + legend.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Introduce a hierarchical risk assessment model with six entity types:
- Risk Assessment: top-level container scoped to an organization
- Risk Assessment Scope: sub-container for scoping threat modeling
exercises within an assessment
- Risk Assessment Node: DFD elements typed as ENTITY, BOUNDARY,
ASSET, or DATA within a scope
- Risk Assessment Process: directed data flows between two nodes
- Risk Assessment Threat: descriptive threats attached to a process
with a free-text category (e.g. Confidentiality, Integrity)
- Risk Scenario: thin join linking a threat to a risk from the
register, carrying only a name and description
Risk scoring (likelihood, impact, treatment) remains on the existing
Risk entity. Threats are purely descriptive. Risk Scenarios connect
the threat model to the risk register without duplicating scores.
Backend: migration with PG enum for node types, coredata structs,
service layer with full CRUD and validation, GraphQL schema with
18 mutations and paginated connections, authorization actions and
policies, and base_resolvers.go Node dispatch for all entity types.
Frontend: Risk Assessments list page with create dialog, detail page
showing scopes as cards with nodes/processes/threats tables, inline
create/edit/delete actions on all entities, and a Scenarios tab on
the Risk detail page linking threats to risks. Existing RiskGraph.ts
hook file removed in favor of colocated queries in page files.
E2E tests cover CRUD for all entity types, RBAC, and tenant
isolation.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
Replace the cross-entity JOIN in
DetectedTrackers.LoadCommonThirdPartyIDByDomainMatch with two
idiomatic coredata calls: LoadInitiatorDomainsByTrackerPatternID
on DetectedTrackers, then a new CommonThirdPartyDomains.Load with
a CommonThirdPartyDomainFilter. Each entity now queries only its
own table, and the caller orchestrates the lookup.
Document the Load vs LoadAll naming convention and the no
cross-entity JOINs rule in contrib/claude/coredata.md.
Signed-off-by: Émile Ré <emile@probo.com>
Upsert methods now RETURNING all struct columns and scan the result
back into the pointer receiver, keeping the caller in sync with the
actual DB state (id, created_at, etc. from the existing row on
conflict). Insert detection compares the saved original ID with the
returned ID instead of relying on the PostgreSQL-internal xmax column.
Signed-off-by: Émile Ré <emile@probo.com>
Checking only pgErr.Code == "23505" is insufficient when a
table has multiple unique constraints. Always verify
pgErr.ConstraintName as well to avoid mapping unrelated
violations to the wrong sentinel error.
Signed-off-by: Émile Ré <emile@probo.com>
Generate the category list from coredata.ThirdPartyCategories() at
runtime instead of hardcoding it in the prompt text. Type the
TrackerIdentification.Category field as coredata.ThirdPartyCategory so
JSON unmarshaling validates values automatically.
Also documents the .txt.tmpl template file naming convention.
Signed-off-by: Émile Ré <emile@probo.com>
Update go-style guide and cursor rule to clarify that even a single
argument spanning multiple lines must break after the opening
parenthesis. Fix six violations across the branch.
Signed-off-by: Émile Ré <emile@probo.com>
When both pattern matching and domain matching fail to identify a
tracker, an opt-in LLM agent can now attempt identification using
internal database searches and optional web search. The agent returns
structured output (third party name, category, description, confidence)
and the worker auto-creates CommonThirdParty records when needed.
The feature is gated behind the `llm.tracker-mapping.provider` config
field; when unset the worker behaves exactly as before.
Signed-off-by: Émile Ré <emile@probo.com>
When neither pattern nor domain matching finds an existing common
tracker pattern, upsert one with no third party association and a
lower confidence (0.5) so every seen pattern is cataloged.
Also fix table name in LoadByOrganizationIDAndCommonThirdPartyID
(vendors -> third_parties).
Signed-off-by: Émile Ré <emile@probo.com>
Remove redundant resolveThirdParty fallback that re-attempted
resolution with the same inputs, and rename
LoadCommonThirdPartyIDByTrackerPatternID to
LoadCommonThirdPartyIDByDomainMatch to reflect that it joins on
initiator_domain.
Signed-off-by: Émile Ré <emile@probo.com>
Change the unique constraint on common_third_party_domains from (domain)
to (common_third_party_id, domain) so a domain like google.com can be
associated with Google, Google Analytics, Google AdSense, etc.
Also fix Beamer domain (was hotjar.com, now getbeamer.com) and preserve
the original url.Parse error in pgconn DSN parsing.
Signed-off-by: Émile Ré <emile@probo.com>
Add a slug column to common_third_parties for stable cross-source
deduplication. The OCD tracker patterns importer now resolves third
parties via a three-step cascade (slug lookup, domain fallback,
auto-create) instead of silently dropping unmatched links.
Signed-off-by: Émile Ré <emile@probo.com>
- Replace copy-pasted New Relic URLs in Resend third-party entry
with correct resend.com URLs and drop inapplicable fields
- Escape single '%' instead of '%%' in LIKE pattern conversion so
literal percent signs are not treated as wildcards
- Return actual row ID from CommonTrackerPattern.Upsert via
RETURNING id so conflict-path callers get the existing ID
- Add ORDER BY id ASC to vendor-by-common-third-party query for
deterministic LIMIT 1 selection
Signed-off-by: Émile Ré <emile@getprobo.com>
Signed-off-by: Émile Ré <emile@probo.com>
LoadByID and LoadByIDs used 4-space indentation and were missed
by the bulk column addition. The struct field existed but the
SELECT did not return it, causing pgx scan failures.
Signed-off-by: Émile Ré <emile@getprobo.com>
Signed-off-by: Émile Ré <emile@probo.com>
Poll-based worker that maps org-scoped tracker patterns to the
common knowledge base via pattern matching and domain-based
attribution. Populates initiator_domain on detected trackers
at report time. Resolves org-scoped vendors through the common
third party link.
Signed-off-by: Émile Ré <emile@getprobo.com>
Signed-off-by: Émile Ré <emile@probo.com>
CommonThirdPartyDomain maps eTLD+1 domains to third parties for
fast domain-based attribution. CommonTrackerPattern stores the
global tracker knowledge base with glob/exact matching support.
Signed-off-by: Émile Ré <emile@getprobo.com>
Signed-off-by: Émile Ré <emile@probo.com>
Introduces common_third_party_domains (CITEXT domain aliases per
third party), common_tracker_patterns (global tracker knowledge
base), and new FK columns on tracker_patterns (third_party_id,
common_tracker_pattern_id), vendors (common_third_party_id), and
detected_trackers (initiator_domain) to support automated
tracker-to-vendor attribution.
Signed-off-by: Émile Ré <emile@getprobo.com>
Signed-off-by: Émile Ré <emile@probo.com>
- Require CLIENT_SECRET for new access-review connectors
- Use Heroku account UUID as ExternalID
- Bump GitHub orgs picker to per_page=100
- Drop Snyk, Ramp, Lever, Deel access-review providers
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>