If you don't know an access path exists, you can't secure it. Before I design a single control, I run discovery: map every identity provider, every shadow path around that provider, and every process that grants or revokes access — because the gaps almost always live in the paths nobody thought to document.
// the check I run before trusting any "who has access" answer:
// does this path actually go through the identity provider, or around it?
interface AccessPath {
resource: string;
grantedVia: "idp" | "direct-invite" | "third-party-integration";
reviewed: boolean;
}
function flagUnreviewedPaths(paths: AccessPath[]): AccessPath[] {
return paths.filter((p) => p.grantedVia !== "idp" && !p.reviewed);
}
That function is the entire point of discovery: everything that comes back non-empty is access you didn't know to audit.
What discovery actually involves
Working through this on a mid-size B2B org, the first question is which system is actually the identity provider. In this case it was Okta, bought specifically to provide federated sign-on for employees. That's the easy part. The harder question is everything that grants access without going through the IdP: GitHub collaborator invites for external contributors, Slack single- and multi-channel guest access for contractors, and a handful of third-party integrations with their own identity stores nobody had inventoried.
What discovery found
Three user types existed, but the account attributes didn't reliably distinguish between them — meaning access decisions couldn't actually be automated on user type, because the data to automate on wasn't clean. Non-employee and non-human accounts had lifecycle processes that didn't match how the business actually used them, which was the single largest source of avoidable risk in the findings.
There was no central inventory of applications — no single place that recorded what auth methods an app supported or how its users were provisioned. Application access decisions were happening ad hoc, and IAM wasn't in the room during procurement, so new tools showed up with their own login flows before anyone flagged it.
On process: no feedback loop existed to catch when provisioning drifted from policy, no documentation mapped lifecycle processes to the actual entry points into the org, and provisioning itself was still manual in the places that mattered most.
None of this happens in a vacuum, either — the findings only matter in the context of what the business actually needed: a flexible, secure way for employees to work, an access model agile enough to keep up with rapid growth, SOC compliance because it was a requirement for landing key customers, and a leadership team that wanted the same data-driven approach they applied everywhere else extended into IAM. Discovery findings that don't map back to those objectives are just a list of problems — tying them together is what turns the list into a roadmap.
How I'd Engineer This Today
The methodology above still holds for human identities. It falls apart the moment autonomous agents start calling tools on your network, because an agent's access path isn't a static list — it's whatever the model decides to invoke at runtime, based on context nobody pre-approved line by line.
Traditional discovery looks backward at logs to reconstruct what happened. Agent security needs the boundary enforced before the call executes, not audited after:
import { z } from "zod";
const ToolCallSchema = z.object({
toolName: z.string(),
requestedResource: z.string(),
agentIdentityToken: z.string(),
});
async function verifyAgentBoundary(toolCall: unknown): Promise<boolean> {
const parsed = ToolCallSchema.safeParse(toolCall);
if (!parsed.success) {
console.error("Malformed agent tool call — aborting before execution.");
return false;
}
const { toolName, requestedResource, agentIdentityToken } = parsed.data;
return checkContextualIAM(agentIdentityToken, toolName, requestedResource);
}
If I were running this discovery engagement now, I'd decouple the agent's reasoning loop from its execution tools with an isolated proxy sidecar in between. Every outbound tool call has to cross that boundary, gets validated against a strict schema, and gets logged with intent attached — not just the raw API call, but which task graph it belongs to. That turns identity discovery from a point-in-time inventory exercise into something closer to a running perimeter: you're not asking "who has access" once a quarter, you're enforcing the answer on every single call.