Current section

Files

Jump to
mob_dev lib mob_dev security_scan layers gradle_deps.ex
Raw

lib/mob_dev/security_scan/layers/gradle_deps.ex

defmodule MobDev.SecurityScan.Layers.GradleDeps do
@moduledoc """
Audits Android dependencies via `osv-scanner` recursively over
the `android/` directory.
## What gets scanned
`osv-scanner` understands these Android-relevant manifests:
* `gradle.lockfile` — the result of Gradle's [dependency locking][1].
Captures the exact transitive dep tree.
* `buildscript-gradle.lockfile` — same idea, for buildscript classpath.
* `pom.xml` — Maven, occasionally appears in Gradle projects.
Mob's Android template does NOT enable dependency locking by default,
so a fresh `mix mob.new` app will report `:not_applicable` for this
layer until the user opts in. The layer's notes spell out the
remediation.
## Enabling Gradle dependency locking
// android/build.gradle
allprojects {
configurations.all {
resolutionStrategy.activateDependencyLocking()
}
}
// android/app/build.gradle
dependencyLocking {
lockAllConfigurations()
}
Then `cd android && ./gradlew :app:dependencies --write-locks`
creates `gradle.lockfile`.
[1]: https://docs.gradle.org/current/userguide/dependency_locking.html
"""
@behaviour MobDev.SecurityScan.Layer
alias MobDev.SecurityScan.{LayerResult, OsvScanner}
@impl true
def name, do: :gradle_deps
@impl true
def run(opts) do
project_root = Keyword.get(opts, :project_root, File.cwd!())
android_dir = Path.join(project_root, "android")
if File.dir?(android_dir) do
run_scan(android_dir, opts)
else
%LayerResult{
name: :gradle_deps,
status: :not_applicable,
notes: ["no android/ directory at #{android_dir}"]
}
end
end
defp run_scan(android_dir, opts) do
osv_scan = Keyword.get(opts, :osv_scan_fn, &OsvScanner.scan/3)
case osv_scan.({:directory, android_dir}, :gradle_deps, []) do
{:ok, findings} when findings == [] ->
%LayerResult{
name: :gradle_deps,
status: :ok,
findings: [],
tools_used: ["osv-scanner"],
notes: notes_with_lockfile_guidance(android_dir)
}
{:ok, findings} ->
%LayerResult{
name: :gradle_deps,
status: :ok,
findings: findings,
tools_used: ["osv-scanner"],
notes: ["osv-scanner: #{length(findings)} finding(s) under #{android_dir}"]
}
{:error, :not_installed} ->
%LayerResult{
name: :gradle_deps,
status: :tool_missing,
notes: [
"osv-scanner not installed — install: brew install osv-scanner",
"without it Android Gradle deps are not audited"
]
}
{:error, {:scan_failed, reason}} ->
%LayerResult{
name: :gradle_deps,
status: :error,
tools_used: ["osv-scanner"],
error: "osv-scanner failed: #{reason}"
}
{:error, {:not_found, path}} ->
%LayerResult{
name: :gradle_deps,
status: :not_applicable,
notes: ["target path missing: #{path}"]
}
end
end
defp notes_with_lockfile_guidance(android_dir) do
lockfile = Path.join([android_dir, "app", "gradle.lockfile"])
base = "osv-scanner ran cleanly under #{android_dir} (0 findings)"
if File.exists?(lockfile) do
[base, "scanned manifests including gradle.lockfile"]
else
[
base,
"NOTE: no gradle.lockfile present — only declared deps in build.gradle were checked",
"for transitive coverage, enable Gradle dependency locking and run `./gradlew :app:dependencies --write-locks`"
]
end
end
end