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>
The contrib/claude/commit.md guideline already required signing,
but relying on local git config (format.signoff, commit.gpgsign)
silently produced unsigned commits on machines without that
config. Make the requirement explicit so agents always pass both
flags.
Signed-off-by: Émile Ré <emile@probo.com>
Unrelated third-party trackers that happened to share a leading
separator run (e.g. __support__, __darkreader__wasEnabledForHost,
__EXT_APP_REFRESH_BLACK_SUB_DOMAINS__) were being merged under
overly broad globs such as __* because templateCandidates emitted
every prefix at each '_' or '-' position without requiring any
fixed anchor.
Add a templateHasFixedAnchor helper and apply it to both
templateCandidates loops and the heuristicTemplate result so
candidates consisting solely of '_', '-', and '*' are rejected.
Signed-off-by: Émile Ré <emile@probo.com>
Wrap script/iframe/img/link/source IDL setters, setAttribute, the four
HTML-parsing entry points, fetch, XHR, and sendBeacon so extension stacks
are visible at the synchronous call site. Coordinate via an
extensionElements WeakSet and an extensionUrls Set so the existing
MutationObserver and PerformanceObserver paths drop the same resources.
Signed-off-by: Émile Ré <emile@probo.com>
The cookie detector tags every detection with a source (script,
pre-existing, http) but the storage detector did not, so storage
rows always landed in detected_trackers with source NULL even though
the SDK already distinguishes wrapper writes from pre-load scans.
Plumb a "script"/"pre-existing" source from the storage detector
through the report endpoint into DetectedStorageItem, so the column
gets populated for localStorage, sessionStorage, indexedDB and
cacheStorage entries. No schema change is needed: detected_trackers
already accepts CookieSource values regardless of tracker_type, and
the existing row renders the badge as soon as it is non-null.
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>
The Node resolver was manually constructing a scope from the object
ID, both at the top of the function and again inside several closures.
Since r.authorize already returns the resolved scope, pass it into the
loadNode closures and drop the duplicate construction.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Describe AuthorizeBatch semantics (all-or-nothing, single-entity-type,
single-organization, batch attribute requirement, audit logging), the
authz.NewBatchAuthorizeFunc / WithBatch* helpers, the MCP
Resolver.AuthorizeBatch entry point, and the new batch-style
AuthorizationAttributes implementation contract (non-empty,
deduplicated, same-entity-type input; only found rows returned).
Signed-off-by: Bryan Frimin <bryan@probo.com>
Resolving a typical Console GraphQL query triggers many parallel
authorize calls (one per resource per field resolver). This commit
collapses them via a dataloader: parallel calls within the same
request are gathered into a single iam.Authorizer.AuthorizeMulti pass,
and only fall back to per-item Authorize when AuthorizeMulti rejects
the whole batch (e.g. mixed organizations).
The loader key encodes resource id, action, options, and a canonical
JSON-encoded attribute map so logically identical calls share a key
while differing ones do not. The loader is created without caching so
repeated calls within a request still produce one audit log entry per
call. dataloader.NewAuthorizeFunc preserves the existing
authz.AuthorizeFunc signature and error mapping.
Signed-off-by: Bryan Frimin <bryan@probo.com>
Add authz.NewBatchAuthorizeFunc — the batch counterpart to the
existing AuthorizeFunc — together with WithBatchAttr,
WithBatchSkipAssumptionCheck, and WithBatchDryRun options. It maps the
new batch errors (mixed organization, empty batch, unsupported
resource type) to GraphQL Invalid responses, and reuses the existing
mappings for ErrAssumptionRequired / ErrInsufficientPermissions /
ErrResourceNotFound.
Plumb the new function into the Connect and Console resolvers and add
Resolver.AuthorizeBatch to the MCP resolver with equivalent error
mapping for tool callers.
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>
Introduce ErrMixedOrganizationBatch, ErrMixedEntityTypeBatch,
ErrEmptyResourceBatch, and ErrBatchAuthorizationUnsupportedResourceType
along with their constructors. These errors will be raised by the
upcoming AuthorizeBatch path and carry enough structured fields for
GraphQL/MCP wrappers to map them to user-facing error codes.
Cover their Error() formatting alongside the existing single-resource
authorization errors.
Signed-off-by: Bryan Frimin <bryan@probo.com>
These aliases (`map[string]string` and `map[gid.GID]Attributes`) give
batch authorization call sites readable types when loading and
returning per-resource condition attributes. ConditionContext now uses
the alias instead of the bare map type, with no behavior change.
Also extend policy tests to cover ResourcePattern.MatchesResource,
comma-separated value handling for In/NotIn, unresolved-reference
fallthrough, and resolveKey/resolveValue.
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>
Expose the full risk assessment hierarchy (assessments, scopes, nodes,
processes, threats, scenarios) with CRUD operations and scenario
linking across all three interfaces.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.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>