Guide 168 decision guide

How to handle submodules and missing dependencies in code review

Handle missing dependencies and submodules as explicit evidence gaps, with bounded acquisition requests, alternative artifacts and a clear point to stop investigation.

Treat a missing submodule or dependency as a named evidence gap, then decide whether that gap could change the security question you are answering. A parent Git repository pins a submodule commit, but that pin is not the submodule's source. Read the parent path, gitlink commit, and .gitmodules mapping; obtain the exact child commit only through an authorized, bounded source path. If you cannot inspect it, do not assume either that its controls exist or that it is vulnerable. State which claim remains unresolved and who can supply the missing code or behavior.

The same discipline applies to a private package, generated client, cloud policy, or deployed service absent from the checkout. A lockfile can identify a version or artifact, yet does not prove how the dependency behaves in this application. When the missing component governs an authorization decision or effect, the review is partial. When it is unrelated to the bounded question, explain why it can remain outside this review without calling the whole repository covered.

Worked decision: a private policy module sits between request and file

Illustrative case, not a customer repository: A security engineer reviews atlas-export for one question: can account A request account B's export file? The API route is in the parent repository. It calls authorizeExport from libs/account-policy, which appears as a Git submodule. The parent has a .gitmodules URL for a private repository and a gitlink to commit P7. The review account can read the parent but not the child. A separate package-lock.json also lists an object-storage SDK. The export worker's deployed image manifest is not in either repository.

Git defines a submodule as a separate repository whose history and working tree remain separate. The superproject records a gitlink object name for the expected child commit, while .gitmodules provides path and URL information. A directory in a checkout may be empty even though the parent contains a gitlink. That empty directory is not evidence that the deployed application lacks authorization code.

The engineer writes the dependency boundary before making any verdict:

Missing or linked item Known from parent snapshot Security role in this case Decision
libs/account-policy submodule Path, URL hint, pinned child commit P7 May allow or deny cross-account export Blocking: obtain approved read access to P7
Object-storage SDK Locked package version and resolved artifact Defines API behavior, not application ownership policy by itself Read vendor API contract if needed; no install
Export worker image Queue call and image name only Determines whether file release is checked again Blocking: map deployed image to source
docs/old-policy symlink Link entry and target path Might point to historical documentation Do not follow; relevance needs owner confirmation
Unrelated UI icon package Manifest entry only No plausible edge to export authorization Outside this bounded claim; still inventoried

This table tells the AppSec lead why the review cannot yet approve a finding or dismiss the alert. It also prevents a common shortcut: silently switching from commit P7 to whatever the child repository's default branch shows today.

Distinguish four kinds of missing context

A submodule is a separate source snapshot. Record the parent commit, gitlink path and pinned child commit. Treat the URL as a location hint subject to repository policy, not an instruction to fetch arbitrary hosts. A relative URL can resolve against the parent origin, as Git's .gitmodules manual explains. If the child cannot be acquired at the pinned commit, document whether it is private, deleted, inaccessible, or outside the approved source scope. Those are different reasons and imply different owners.

A package is an artifact and an API boundary. npm's lockfile documentation says the lockfile describes the dependency tree and can record resolved locations, integrity values, and Git commits. It does not by itself show whether the deployed image used that lockfile, whether a runtime flag changed behavior, or whether a wrapper in the application supplied a missing permission check. Read the package's primary documentation for the method contract; request the exact artifact or trusted source when implementation behavior is decisive. Do not run an install just to “see what happens” during a source-only review.

A generated client is a copy of a contract, not necessarily the contract in production. Find the schema and generator version if they matter. The generated method signature may show that accountId is sent, but the server may ignore it, rederive it, or trust it unsafely. Trace to the server-side control rather than treating client code as proof of enforcement.

A symlink is a link, not automatic inclusion. Git's data model documentation distinguishes symbolic-link mode from regular file and gitlink modes. Record the link target as data. Do not follow it outside the authorized snapshot or let a source reader escape its path boundary. A link to a file that is in scope can be mapped explicitly after its target and content are checked; a link to an unavailable path remains a gap.

Make an acquisition decision before reading more

For a blocking component, ask the repository owner to authorize the exact child repository and commit, with a permitted source location and review purpose. The owner of atlas-export may be able to arrange child-repository access or provide a verified immutable archive through an approved channel. Repository access generally exposes more than one commit: if the authorization is limited to P7, an owner-prepared bounded snapshot may better match it. A reviewer promise to read only P7 is not a provider-enforced commit permission. Keep the permission request narrow: the goal is to inspect authorizeExport and its called controls at P7, not to browse the private team's entire history.

Read child source as untrusted data in the same bounded workflow as the parent. Do not blindly recurse through submodules, run build instructions, execute hooks, or install dependencies. The child may itself contain another gitlink, unusual encoding, large files, or a link. Each new boundary receives its own scope decision. If the acquisition fails, record the reason and continue with independent evidence that does not depend on inventing that code's behavior.

Check the completeness of the listing as well. On GitHub, a recursive Trees API response can be marked truncated; the documented continuation is to fetch subtrees. Missing files in a capped response should be labeled not inventoried, not “not present.” The same principle applies to any provider or archive with limits.

Watch the gap change as evidence arrives

The atlas owner first supplies an archive named account-policy-latest.zip. Its README says “P7,” but the archive has no trusted mapping to the parent gitlink. Comparing its contents with the accessible default branch identifies P9, a later change. The reviewer rejects the archive as evidence of P7's behavior. Useful new code can still be the wrong evidence.

Next, the owner supplies an authorized immutable archive with a verifiable mapping to child commit P7. The reviewer reopens the parent caller and child function together. The fictional code has this contract:

authorizeExport(member, exportId) → { allowed, storedAccount, storedExportId }

P7 compares the export's stored account to the member's authorized account. The parent checks allowed before continuing, but then enqueues request.exportId, not a value from the decision. Is that a bug? In this example the request object is immutable within the handler, and the same exact ID was passed into P7. The values agree. Merely preferring the returned ID is not proof of an exploitable mismatch. If an intervening transformation, mutable shared object or alternate caller can change the ID, that would be a new edge to inspect.

This evidence resolves the API job-creation question for the pinned parent/P7 pair. The worker remains unmapped, so end-to-end delivery is still incomplete. The review is no longer simply “blocked on auth”; its conclusion should improve when the blocking artifact arrives.

Gap First disposition Later evidence Revised disposition
Policy child Blocking P9 archive supplied Still blocking for P7; record rejected evidence and reason.
Policy child Blocking Verified P7 plus parent arguments, branch and immutable selector API authorization question reviewed; unauthorized selection rejected for this call.
Storage SDK Conditional Exact locked version's API contract explains the URL method May settle argument semantics; does not prove application ownership policy or deployed use.
Worker Blocking Image name only Still blocking for delivery; request running digest and source mapping.
Icon package Bounded non-blocking No edge to job authorization or delivery in this scope Remains inventoried and deferred; no package-wide security conclusion.

The storage SDK is an example of alternate evidence with a limited reach. If the question is whether an API parameter is an object key or an account boundary, primary documentation for the actual version may settle it. If the question is whether the deployed worker supplies an authorized key, documentation cannot replace the caller and artifact evidence. Do not install the dependency just to turn an evidence gap into an execution experiment.

Use three dispositions consistently: blocking when the component can change the current conclusion; bounded non-blocking when no relevant edge is identified within the declared scope; and alternate evidence sufficient for a named edge when an authoritative contract or independent control actually resolves that edge. These states belong to a question, not permanently to a package.

Set a useful stopping rule for unavailable source

If the owner cannot provide P7, ask whether a narrower evidence path can answer the current decision. A matched contract may clarify argument semantics; an independently enforced downstream object check may defeat one proposed effect; an authorized synthetic validation may observe selected behavior. Record the proposition each alternative supports and the unseen implementation that remains outside it.

Do not endlessly repeat acquisition attempts that fail for the same known permission boundary. Preserve the exact missing subject and owner, then continue work that does not depend on it. If the missing control is decisive for release assurance, the accountable release owner must choose whether to obtain access, accept a qualified decision or delay that claim. The reviewer should not silently convert the blocker into a favorable assumption.

A useful handoff gives the next person the parent/child identity, failed acquisition reason, requested method and the decision waiting on it. When access later arrives, they can resume the caller-to-control trace without rebuilding the entire investigation. That is the practical value of treating a missing dependency as a named evidence gap.

Report a partial review without burying it

The final note should say: “At parent commit X and verified child P7, the API checks the stored export owner before creating the job. The initial P9 archive was rejected as evidence for this version. The deployed worker remains unmapped, so delivery authorization is unresolved. The API result does not clear the full export flow.” Replace X and P7 with the actual immutable identifiers in the private record.

Add a deadline and named owner for each missing artifact. If access remains impossible, the AppSec lead can plan a controlled validation or a compensating review of the deployed system, but should not turn the unresolved source path into a false positive or a verified vulnerability. OWASP's Code Review Guide offers review methods; the value here is using them with a precise boundary and honest result.

For your next review, start with the five-row boundary table and mark each linked component blocking or bounded. Then use the cross-repository request trace to follow the authorized source you obtain. If the missing code changes how much of the application was examined, use the targeted-versus-full decision guide to describe the resulting scope.