You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Aperio's shadow-IT page (/shadow-it) lists every OAuth app a user granted — flat list. It treats each OauthAppGrant row as terminal. But real SaaS-to-SaaS abuse compounds through chains:
A user installs Slack App Alpha. Alpha holds team:write scope. Alpha is itself an OAuth client that grants tokens to a downstream app Beta, which exfils.
A Salesforce Connected App X is installed; X's refresh_token is in turn used by an external integration that talks to GitHub on the customer's behalf.
A GitHub App installs another GitHub App via the org-management API (yes, this is possible) — token holds permissions the original installer never approved.
M365 service principal A holds AppRoleAssignment.ReadWrite.All; A grants itself or another SP an admin scope without a human in the loop.
These chains are the next-frontier SaaS attack class ("SaaSjacking" in some vendor materials, "OAuth supply chain" in others). Commercial SSPMs barely surface them; an OSS engine that maps and alerts on them is genuinely novel.
Goals
OAuth client graph: model OAuth clients as first-class nodes that connect to each other (publisher/installer/delegated-grantee relationships).
Chain discovery: identify chains where an OAuth grant transitively confers access to another OAuth grant.
Chain risk scoring: a chain's risk = function of every node's scope sensitivity (CRITICAL/HIGH/MEDIUM/LOW set from existing graduated risk) and the publisher trust of every link.
Detection rules for the dangerous chain shapes (e.g. "low-trust publisher gates access to a CRITICAL-scope downstream grant").
UI: a "chain explorer" that shows the full transitive grant graph for any starting app or user.
Not detecting runtime token-theft (no traffic-mirror feature) — chain analysis is structural, not behavioral.
Proposed design
What's already there
OauthAppGrant model with provider, externalAppId, appDisplayName, scopes, userEmail, assetId. The per-user, per-app axis is complete.
SecurityAsset type OAUTH_APP with riskScore, containsSensitiveData, isPrivileged, exposureLevel already exposed in the security graph.
Graduated scope risk classification (CRITICAL/HIGH/MEDIUM/LOW sets) lives in workers/ingestion-worker.tsgoogleOauthGrantRisk and similar — extend rather than rebuild.
New domain model
enumOauthClientPublisher {FIRST_PARTY// ships with the provider (e.g. Google's own clients)VERIFIED_VENDOR// platform-verified marketplace appUNVERIFIED_VENDOR// marketplace-listed but unverifiedTENANT_INTERNAL// built by the customer themselvesUNKNOWN}modelOauthClient {idString@id@default(cuid())organizationIdString@map("organization_id")providerSaaSProviderexternalClientIdString@map("external_client_id")@db.VarChar(255)displayNameString?@db.VarChar(255)publisherKindOauthClientPublisher@default(UNKNOWN)@map("publisher_kind")publisherDomainString?@map("publisher_domain")@db.VarChar(255)trustScoreInt@default(50)@map("trust_score")// 0-100; threat-intel input (#51) feeds inisOauthBlocklistedBoolean@default(false)@map("is_oauth_blocklisted")observedAtDateTime@default(now())@map("observed_at")organizationOrganization@relation(...)@@unique([organizationId, provider, externalClientId])@@index([organizationId, provider, publisherKind])@@map("oauth_clients")}enumOauthGrantEdgeKind {INSTALLED_BY// a Person installed this clientDELEGATED_BY// this client acts on behalf of another clientPUBLISHED_BY// this client's publisher is the same legal entity as anotherIMPERSONATES// domain-wide-delegation styleEXTENDS_VIA_FRAMEWORK// e.g. Slack manifest depends on another app}modelOauthGrantEdge {idString@id@default(cuid())organizationIdString@map("organization_id")fromClientIdString@map("from_client_id")toClientIdString@map("to_client_id")kindOauthGrantEdgeKindevidenceJsonobservedAtDateTime@default(now())@map("observed_at")organizationOrganization@relation(...)@@unique([organizationId, fromClientId, toClientId, kind])@@index([organizationId, toClientId])@@map("oauth_grant_edges")}
Chain discovery
A new internal/oauthchains/ Go package:
On every shadow-IT scan, materialize OauthClient rows from OauthAppGrant.
Per-app chain view — on any OauthAppGrant detail page, show its transitive reach.
Phasing
Phase
Scope
P1
OauthClient model + materialization from existing OauthAppGrant rows; publisher-kind classification (heuristic from clientId domain); chain summary table; /shadow-it/chains list page
Should a chain that crosses tenants (Aperio sees the same OAuth clientId across multiple of its customers) inform the trust score? (Probably yes, anonymized.)
Problem
Aperio's shadow-IT page (
/shadow-it) lists every OAuth app a user granted — flat list. It treats eachOauthAppGrantrow as terminal. But real SaaS-to-SaaS abuse compounds through chains:team:writescope. Alpha is itself an OAuth client that grants tokens to a downstream app Beta, which exfils.refresh_tokenis in turn used by an external integration that talks to GitHub on the customer's behalf.AppRoleAssignment.ReadWrite.All; A grants itself or another SP an admin scope without a human in the loop.These chains are the next-frontier SaaS attack class ("SaaSjacking" in some vendor materials, "OAuth supply chain" in others). Commercial SSPMs barely surface them; an OSS engine that maps and alerts on them is genuinely novel.
Goals
Non-goals
Proposed design
What's already there
OauthAppGrantmodel withprovider,externalAppId,appDisplayName,scopes,userEmail,assetId. The per-user, per-app axis is complete.SecurityAssettypeOAUTH_APPwithriskScore,containsSensitiveData,isPrivileged,exposureLevelalready exposed in the security graph.workers/ingestion-worker.tsgoogleOauthGrantRiskand similar — extend rather than rebuild.New domain model
Chain discovery
A new
internal/oauthchains/Go package:OauthClientrows fromOauthAppGrant.clientIdlineage from OAuth consent records; service-account → DWD mapping (already partially modeled).oauth2PermissionGrantsandappRoleAssignmentsbetween service principals.OauthGrantEdge.Chain rules (built into #47 once it lands)
oauth.unverified_chain_to_critical_scopeUNVERIFIED_VENDORand any node holds a CRITICAL scope.oauth.first_party_proxied_to_third_partyoauth.blocklisted_app_in_chainoauth.cross_provider_chain_to_data_resourceDATA_RESOURCEasset.oauth.transitive_admin_scopeUI surface
/shadow-it/chains— list of discovered chains, sortable by length, max risk, min trust.OauthAppGrantdetail page, show its transitive reach.Phasing
OauthClientmodel + materialization from existingOauthAppGrantrows; publisher-kind classification (heuristic from clientId domain); chain summary table;/shadow-it/chainslist pageAgentProposalgateOpen questions
clientIdacross multiple of its customers) inform the trust score? (Probably yes, anonymized.)References
OauthAppGrant,SecurityAsset,Organization, the existing graduated scope-risk classifier.