Current section

Files

Jump to
mob_dev lib mob_dev security_scan layers swift_deps.ex
Raw

lib/mob_dev/security_scan/layers/swift_deps.ex

defmodule MobDev.SecurityScan.Layers.SwiftDeps do
@moduledoc """
Audits iOS dependencies via `osv-scanner` recursively over the
`ios/` directory.
## What gets scanned
`osv-scanner` understands:
* `Package.resolved` — Swift Package Manager (when SwiftPM is used)
* `Podfile.lock` — CocoaPods
Mob's iOS template does not depend on either by default — the iOS
bridge is built with raw `.m` / `.swift` files plus the bundled OTP
static libs (libcrypto.a, libbeam.a, etc.). Those static libs are
audited by the `:bundled_runtime` layer; this layer only covers
*application-level* iOS dependencies.
In a stock Mob app this layer typically reports `:not_applicable`,
which is the correct signal — there's no iOS dependency manifest
to audit because the app pulls nothing from CocoaPods/SwiftPM.
"""
@behaviour MobDev.SecurityScan.Layer
alias MobDev.SecurityScan.{LayerResult, OsvScanner}
@impl true
def name, do: :swift_deps
@impl true
def run(opts) do
project_root = Keyword.get(opts, :project_root, File.cwd!())
ios_dir = Path.join(project_root, "ios")
cond do
not File.dir?(ios_dir) ->
%LayerResult{
name: :swift_deps,
status: :not_applicable,
notes: ["no ios/ directory at #{ios_dir}"]
}
not has_swift_manifest?(ios_dir) ->
%LayerResult{
name: :swift_deps,
status: :not_applicable,
notes: [
"no Package.resolved or Podfile.lock under #{ios_dir}",
"Mob iOS apps typically have neither — bundled OpenSSL/SQLite are audited by :bundled_runtime"
]
}
true ->
run_scan(ios_dir, opts)
end
end
defp has_swift_manifest?(ios_dir) do
paths = [
Path.join([ios_dir, "Package.resolved"]),
Path.join([ios_dir, "**/Package.resolved"]),
Path.join([ios_dir, "Podfile.lock"]),
Path.join([ios_dir, "**/Podfile.lock"])
]
Enum.any?(paths, &(Path.wildcard(&1) != []))
end
defp run_scan(ios_dir, opts) do
osv_scan = Keyword.get(opts, :osv_scan_fn, &OsvScanner.scan/3)
case osv_scan.({:directory, ios_dir}, :swift_deps, []) do
{:ok, findings} ->
%LayerResult{
name: :swift_deps,
status: :ok,
findings: findings,
tools_used: ["osv-scanner"],
notes: ["osv-scanner: #{length(findings)} finding(s) under #{ios_dir}"]
}
{:error, :not_installed} ->
%LayerResult{
name: :swift_deps,
status: :tool_missing,
notes: ["osv-scanner not installed — install: brew install osv-scanner"]
}
{:error, {:scan_failed, reason}} ->
%LayerResult{
name: :swift_deps,
status: :error,
tools_used: ["osv-scanner"],
error: "osv-scanner failed: #{reason}"
}
{:error, {:not_found, path}} ->
%LayerResult{
name: :swift_deps,
status: :not_applicable,
notes: ["target path missing: #{path}"]
}
end
end
end