app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 32e461b30288b12af237a5be9baaa1dda79ded80
parent f481cf7d57a6a68d46330a2bc96e274ba46b2ab2
Author: triesap <tyson@radroots.org>
Date:   Tue,  4 Aug 2026 02:03:09 +0000

release: add fail-closed evidence gates

- enforce Rust and JVM advisory and license policy at release readiness
- verify Developer ID signatures and stapled notarization evidence
- keep release credentials outside source and configuration caches
- expose honest audit, license, and release checks through Make

Diffstat:
MMakefile | 18++++++++++++++++--
Mapp/desktop/build.gradle.kts | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/licenses/allowed-licenses.json | 8++++++++
Acore/deny.toml | 29+++++++++++++++++++++++++++++
Mgradle/libs.versions.toml | 4++++
5 files changed, 174 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile @@ -4,10 +4,10 @@ GRADLE ?= ./gradlew CARGO ?= cargo CARGO_MANIFEST := core/Cargo.toml -.PHONY: help doctor format format-fix lint test check build bindings dev run package clean +.PHONY: help doctor format format-fix lint test check build bindings dev run audit licenses package release-check clean help: - @printf '%s\n' doctor format format-fix lint test check build bindings dev run package clean + @printf '%s\n' doctor format format-fix lint test check build bindings dev run audit licenses package release-check clean doctor: java -version @@ -46,9 +46,23 @@ dev: doctor run: doctor $(GRADLE) :app:desktop:run +audit: doctor + $(CARGO) audit --file core/Cargo.lock + $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories + $(GRADLE) --no-daemon --no-configuration-cache :app:desktop:dependencyCheckAnalyze + +licenses: doctor + $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml licenses sources + $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:checkLicense + package: check $(GRADLE) --no-daemon :app:desktop:verifyMacOsPackage +release-check: doctor + $(CARGO) audit --file core/Cargo.lock + $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories licenses sources + $(GRADLE) --no-daemon --no-parallel --no-configuration-cache :app:desktop:releaseReadiness + clean: doctor $(CARGO) clean --manifest-path $(CARGO_MANIFEST) $(GRADLE) --no-daemon clean diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts @@ -1,3 +1,4 @@ +import com.github.jk1.license.filter.SpdxLicenseBundleNormalizer import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.file.ConfigurableFileCollection @@ -27,6 +28,30 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.detekt) alias(libs.plugins.ktlint) + alias(libs.plugins.license.report) + alias(libs.plugins.owasp.dependency.check) +} + +licenseReport { + projects = arrayOf(project) + configurations = arrayOf("runtimeClasspath") + filters = arrayOf(SpdxLicenseBundleNormalizer()) + allowedLicensesFile = rootProject.layout.projectDirectory.file("config/licenses/allowed-licenses.json") +} + +dependencyCheck { + failBuildOnCVSS = 0.0F + failOnError = true + formats = listOf("HTML", "JSON") + scanConfigurations = listOf("runtimeClasspath") + skipTestGroups = true + providers.environmentVariable("NVD_API_KEY").orNull?.takeIf(String::isNotBlank)?.let { + nvd.apiKey = it + } +} + +tasks.matching { it.name.startsWith("dependencyCheck") }.configureEach { + notCompatibleWithConfigurationCache("Advisory data and environment-only credentials must not be cached") } configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> { @@ -394,6 +419,82 @@ abstract class VerifyMacOsPackage : DefaultTask() { } } +abstract class VerifyMacOsDeveloperIdSignature : DefaultTask() { + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val appDirectory: DirectoryProperty + + @TaskAction + fun verify() { + val app = appDirectory.get().asFile + commandOutput("/usr/bin/codesign", "--verify", "--deep", "--strict", "--verbose=2", app.absolutePath) + val signature = commandOutput("/usr/bin/codesign", "--display", "--verbose=4", app.absolutePath) + require(!signature.contains("Signature=adhoc")) { + "Release application is ad-hoc signed; a Developer ID Application signature is required" + } + require(signature.lineSequence().any { it.startsWith("Authority=Developer ID Application:") }) { + "Release application is not signed by a Developer ID Application identity" + } + require( + signature.lineSequence().any { + it.startsWith("TeamIdentifier=") && it != "TeamIdentifier=not set" + }, + ) { + "Release application signature has no Apple team identifier" + } + } + + private fun commandOutput(vararg command: String): String { + val process = + ProcessBuilder(*command) + .redirectErrorStream(true) + .start() + val output = + process.inputStream + .bufferedReader() + .use { it.readText() } + .trim() + require(process.waitFor() == 0) { "Code-signature verification failed: $output" } + return output + } +} + +abstract class VerifyMacOsNotarization : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val diskImage: RegularFileProperty + + @TaskAction + fun verify() { + val image = diskImage.get().asFile + commandOutput("/usr/bin/xcrun", "stapler", "validate", image.absolutePath) + commandOutput( + "/usr/sbin/spctl", + "--assess", + "--type", + "open", + "--context", + "context:primary-signature", + "--verbose=2", + image.absolutePath, + ) + } + + private fun commandOutput(vararg command: String): String { + val process = + ProcessBuilder(*command) + .redirectErrorStream(true) + .start() + val output = + process.inputStream + .bufferedReader() + .use { it.readText() } + .trim() + require(process.waitFor() == 0) { "Notarization verification failed: $output" } + return output + } +} + dependencies { implementation(compose.desktop.currentOs) implementation(libs.compose.foundation) @@ -530,3 +631,19 @@ val verifyMacOsPackage by tasks.registering(VerifyMacOsPackage::class) { packageDirectory.set(layout.buildDirectory.dir("compose/binaries/main/dmg")) expectedFileName.set("$applicationName-$installableVersion.dmg") } +val verifyMacOsDeveloperIdSignature by tasks.registering(VerifyMacOsDeveloperIdSignature::class) { + dependsOn(verifyMacOsPackage) + appDirectory.set(layout.buildDirectory.dir("compose/binaries/main/app/$applicationName.app")) +} +val verifyMacOsNotarization by tasks.registering(VerifyMacOsNotarization::class) { + dependsOn(verifyMacOsPackage) + diskImage.set(layout.buildDirectory.file("compose/binaries/main/dmg/$applicationName-$installableVersion.dmg")) +} +tasks.register("releaseReadiness") { + dependsOn( + "checkLicense", + "dependencyCheckAnalyze", + verifyMacOsDeveloperIdSignature, + verifyMacOsNotarization, + ) +} diff --git a/config/licenses/allowed-licenses.json b/config/licenses/allowed-licenses.json @@ -0,0 +1,8 @@ +{ + "allowedLicenses": [ + { "moduleLicense": "Apache-2.0" }, + { "moduleLicense": "BSD-2-Clause" }, + { "moduleLicense": "BSD-3-Clause" }, + { "moduleLicense": "MIT" } + ] +} diff --git a/core/deny.toml b/core/deny.toml @@ -0,0 +1,29 @@ +[advisories] +ignore = [] +unmaintained = "workspace" + +[licenses] +allow = [ + "0BSD", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "CC0-1.0", + "CDLA-Permissive-2.0", + "GPL-3.0-only", + "ISC", + "LGPL-2.1-or-later", + "MIT", + "MPL-2.0", + "Unicode-3.0", + "Unlicense", + "Zlib", +] +confidence-threshold = 0.93 + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = ["https://github.com/rust-nostr/nostr.git"] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -5,6 +5,8 @@ jna = "5.17.0" coroutines = "1.9.0" detekt = "2.0.0-alpha.5" ktlint = "14.2.0" +license-report = "3.1.4" +owasp-dependency-check = "12.2.2" [libraries] compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" } @@ -19,3 +21,5 @@ compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } detekt = { id = "dev.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +license-report = { id = "com.github.jk1.dependency-license-report", version.ref = "license-report" } +owasp-dependency-check = { id = "org.owasp.dependencycheck", version.ref = "owasp-dependency-check" }