Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitguardian.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
paths-ignore:
- "app/build.gradle.kts"
Comment on lines +1 to +2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't exclude the whole Gradle script from secret scanning.

app/build.gradle.kts now wires release-signing inputs. Ignoring the entire file hides any real credential accidentally committed there later; narrow this to the specific false-positive pattern instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitguardian.yml around lines 1 - 2, The .gitguardian.yml currently uses the
paths-ignore entry to skip the entire Gradle script (the "paths-ignore" key with
the "app/build.gradle.kts" pattern); remove that whole-file exclusion and
instead add a narrow allowlist/ignore rule that targets only the known
false-positive token pattern introduced for release signing (e.g., the exact
string or a specific regex for the signing input) so the rest of
app/build.gradle.kts remains scanned; update the "paths-ignore" block to no
longer contain the full filename and replace it with a rule that matches only
the false-positive pattern.

53 changes: 53 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
version: 2

updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"
labels:
- "dependencies"
- "automated"
groups:
minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
gradle-security:
applies-to: security-updates
patterns:
- "*"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
commit-message:
prefix: "ci"
labels:
- "ci"
- "automated"
groups:
actions-minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
actions-security:
applies-to: security-updates
patterns:
- "*"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
Comment on lines +9 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Set explicit read-only token permissions for CI.

This job runs PR code via ./gradlew, but it inherits whatever default GITHUB_TOKEN scope the repository is configured with. Declare contents: read here so the workflow stays least-privileged even if repo defaults change.

🔒 Proposed fix
 jobs:
   build:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     steps:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jobs:
build:
runs-on: ubuntu-latest
steps:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 9 - 12, Add explicit least-privileged
GITHUB_TOKEN permissions for the CI job by declaring permissions: contents: read
for the build job; update the jobs -> build block (the job named "build" that
contains runs-on: ubuntu-latest and steps) to include a permissions entry with
contents: read so the workflow does not inherit broader repo defaults.

- uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Build debug APK
run: ./gradlew assembleDebug

- name: Run lint
run: ./gradlew lintDebug

- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk
69 changes: 69 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Release

on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v4

- name: Compute version from tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
# versionCode: major*10000 + minor*100 + patch (e.g. 1.2.3 -> 10203)
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
CODE=$(( ${MAJOR:-0} * 10000 + ${MINOR:-0} * 100 + ${PATCH:-0} ))
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "name=$VERSION" >> "$GITHUB_OUTPUT"
echo "code=$CODE" >> "$GITHUB_OUTPUT"
Comment on lines +17 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate the tag before deriving VERSION_CODE.

v* accepts tags this parser cannot safely encode (v1.2, vfoo, v1.2.3-rc1), and major*10000 + minor*100 + patch collides once minor or patch reaches 100. Fail fast on a strict vMAJOR.MINOR.PATCH tag, or widen the radix.

💡 Proposed fix
       - name: Compute version from tag
         id: version
         run: |
           TAG="${GITHUB_REF#refs/tags/}"
-          VERSION="${TAG#v}"
-          # versionCode: major*10000 + minor*100 + patch (e.g. 1.2.3 -> 10203)
-          IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
-          CODE=$(( ${MAJOR:-0} * 10000 + ${MINOR:-0} * 100 + ${PATCH:-0} ))
+          if [[ ! "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
+            echo "Expected tag format vMAJOR.MINOR.PATCH, got: $TAG" >&2
+            exit 1
+          fi
+          MAJOR="${BASH_REMATCH[1]}"
+          MINOR="${BASH_REMATCH[2]}"
+          PATCH="${BASH_REMATCH[3]}"
+          if (( MINOR > 99 || PATCH > 99 )); then
+            echo "MINOR and PATCH must be <= 99 for the current versionCode scheme" >&2
+            exit 1
+          fi
+          VERSION="${MAJOR}.${MINOR}.${PATCH}"
+          CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
           echo "tag=$TAG" >> "$GITHUB_OUTPUT"
           echo "name=$VERSION" >> "$GITHUB_OUTPUT"
           echo "code=$CODE" >> "$GITHUB_OUTPUT"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 17 - 27, The tag parsing step
(variables TAG, VERSION, MAJOR, MINOR, PATCH and computed CODE) accepts many
malformed tags and can produce collisions; change the script to first validate
TAG/VERSION against a strict semver patch regex (e.g. ^v[0-9]+\.[0-9]+\.[0-9]+$)
and exit non‑zero with a clear error if it doesn't match, then safely split
VERSION into MAJOR, MINOR, PATCH and compute CODE (or use larger multipliers)
only for validated input so invalid tags like v1.2, vfoo, or v1.2.3-rc1 are
rejected before computing CODE.


- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/release.keystore

- name: Build release AAB
run: ./gradlew bundleRelease -PVERSION_NAME=${{ steps.version.outputs.name }} -PVERSION_CODE=${{ steps.version.outputs.code }}
env:
CASHPILOT_KEYSTORE_PATH: release.keystore
CASHPILOT_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
CASHPILOT_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
CASHPILOT_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: Build release APK
run: ./gradlew assembleRelease -PVERSION_NAME=${{ steps.version.outputs.name }} -PVERSION_CODE=${{ steps.version.outputs.code }}
env:
CASHPILOT_KEYSTORE_PATH: release.keystore
CASHPILOT_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
CASHPILOT_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
CASHPILOT_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: Remove keystore
if: always()
run: rm -f app/release.keystore

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
generate_release_notes: true
files: |
app/build/outputs/bundle/release/app-release.aab
app/build/outputs/apk/release/app-release.apk
42 changes: 38 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,31 @@ android {
applicationId = "com.cashpilot.android"
minSdk = 26 // Android 8.0 — NotificationListenerService, NetworkStatsManager
targetSdk = 35
versionCode = 1
versionName = "0.1.0"

val ciVersionName = findProperty("VERSION_NAME") as String? ?: "0.1.0"
val ciVersionCode = (findProperty("VERSION_CODE") as String?)?.toIntOrNull() ?: 1
versionCode = ciVersionCode
versionName = ciVersionName
}

signingConfigs {
create("release") {
val keystorePath = findProperty("CASHPILOT_KEYSTORE_PATH") as String?
?: System.getenv("CASHPILOT_KEYSTORE_PATH")
val keystorePass = findProperty("CASHPILOT_KEYSTORE_PASSWORD") as String?
?: System.getenv("CASHPILOT_KEYSTORE_PASSWORD")
val keyAliasValue = findProperty("CASHPILOT_KEY_ALIAS") as String?
?: System.getenv("CASHPILOT_KEY_ALIAS")
val keyPass = findProperty("CASHPILOT_KEY_PASSWORD") as String?
?: System.getenv("CASHPILOT_KEY_PASSWORD")

if (keystorePath != null && keystorePass != null && keyAliasValue != null && keyPass != null) {
storeFile = file(keystorePath)
storePassword = keystorePass
keyAlias = keyAliasValue
keyPassword = keyPass
}
Comment on lines +25 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Treat blank signing inputs as missing configuration.

Using only != null accepts empty strings, which can pass this guard but still break signing later with harder-to-debug errors.

Proposed hardening
-            val keystorePath = findProperty("CASHPILOT_KEYSTORE_PATH") as String?
-                ?: System.getenv("CASHPILOT_KEYSTORE_PATH")
-            val keystorePass = findProperty("CASHPILOT_KEYSTORE_PASSWORD") as String?
-                ?: System.getenv("CASHPILOT_KEYSTORE_PASSWORD")
-            val keyAliasValue = findProperty("CASHPILOT_KEY_ALIAS") as String?
-                ?: System.getenv("CASHPILOT_KEY_ALIAS")
-            val keyPass = findProperty("CASHPILOT_KEY_PASSWORD") as String?
-                ?: System.getenv("CASHPILOT_KEY_PASSWORD")
+            val keystorePath = ((findProperty("CASHPILOT_KEYSTORE_PATH") as String?)
+                ?: System.getenv("CASHPILOT_KEYSTORE_PATH"))?.takeIf { it.isNotBlank() }
+            val keystorePass = ((findProperty("CASHPILOT_KEYSTORE_PASSWORD") as String?)
+                ?: System.getenv("CASHPILOT_KEYSTORE_PASSWORD"))?.takeIf { it.isNotBlank() }
+            val keyAliasValue = ((findProperty("CASHPILOT_KEY_ALIAS") as String?)
+                ?: System.getenv("CASHPILOT_KEY_ALIAS"))?.takeIf { it.isNotBlank() }
+            val keyPass = ((findProperty("CASHPILOT_KEY_PASSWORD") as String?)
+                ?: System.getenv("CASHPILOT_KEY_PASSWORD"))?.takeIf { it.isNotBlank() }

             if (keystorePath != null && keystorePass != null && keyAliasValue != null && keyPass != null) {
                 storeFile = file(keystorePath)
                 storePassword = keystorePass
                 keyAlias = keyAliasValue
                 keyPassword = keyPass
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/build.gradle.kts` around lines 25 - 39, The current null checks accept
empty or whitespace-only signing inputs (keystorePath, keystorePass,
keyAliasValue, keyPass) which can later break signing; update the guard to treat
blank strings as missing by replacing the null checks with blank-aware checks
(e.g., use Kotlin's isNullOrBlank) so only non-null, non-blank values are used
when assigning storeFile, storePassword, keyAlias and keyPassword.

}
}

buildTypes {
Expand All @@ -25,6 +48,10 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
val releaseConfig = signingConfigs.findByName("release")
if (releaseConfig?.storeFile != null) {
signingConfig = releaseConfig
}
}
}

Expand All @@ -33,11 +60,18 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
Comment on lines +63 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Android Gradle Kotlin DSL projects, is kotlin { compilerOptions { ... } }valid inside theandroid {} block, or must it be a top-level block? Please check the current Kotlin Gradle plugin / Android Gradle plugin documentation.

💡 Result:

In Android Gradle Kotlin DSL projects, kotlin { compilerOptions { ... } } must be a top-level block, not inside the android {} block. The kotlin {} block is provided by the Kotlin Gradle plugin (e.g., kotlin("android") or kotlin("multiplatform")) and configures Kotlin-specific settings like compiler options at the project extension level. The deprecated android { kotlinOptions { ... } } was inside the android {} block, but the replacement kotlin { compilerOptions { ... } } is top-level, as shown in official Kotlin documentation migration examples for Android projects. With Android Gradle Plugin 9.0+ built-in Kotlin support, the Kotlin plugin may be optional, but when using compilerOptions, the top-level kotlin {} block is still required. Example from Kotlin docs for Android project migration: plugins { id("com.android.application") kotlin("android") } kotlin { compilerOptions { jvmTarget = JvmTarget.fromTarget("17") } } android { // Android config, no kotlinOptions here }

Citations:


🏁 Script executed:

cat -n app/build.gradle.kts | sed -n '50,80p'

Repository: GeiserX/CashPilot-android

Length of output: 1129


🏁 Script executed:

cat -n app/build.gradle.kts | head -75

Repository: GeiserX/CashPilot-android

Length of output: 3008


Move kotlin { compilerOptions {} } out of android {} block.

The kotlin { compilerOptions {} } block is a top-level extension provided by the Kotlin Gradle plugin and must not be nested inside the android {} block. According to official Kotlin and Android Gradle plugin documentation, this configuration should appear at the project root level. The deprecated android { kotlinOptions {} } was inside the Android block, but its modern replacement requires a top-level kotlin {} block.

Proposed fix
-    kotlin {
-        compilerOptions {
-            jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
-        }
-    }
-
     buildFeatures {
         buildConfig = true
         compose = true
     }
 }
+
+kotlin {
+    compilerOptions {
+        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
buildFeatures {
buildConfig = true
compose = true
}
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/build.gradle.kts` around lines 63 - 66, The kotlin { compilerOptions {
jvmTarget.set(...) } } block is currently nested inside the android { } block;
move this top-level Kotlin Gradle plugin configuration out of android by placing
the kotlin { compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } block at the
project/module root (same level as android { } and plugins), removing the nested
version so the Kotlin compilerOptions are defined as a top-level extension
rather than inside the android block.

}

lint {
baseline = file("lint-baseline.xml")
}

buildFeatures {
buildConfig = true
compose = true
}
}
Expand Down
26 changes: 26 additions & 0 deletions app/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint" type="baseline" client="gradle" generated="This file is automatically generated by lint; do not edit">

<issue
id="ProtectedPermissions"
message="Permission is only granted to system apps"
errorLine1=' &lt;uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"'
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="10"
column="22"/>
</issue>

<issue
id="ProtectedPermissions"
message="Permission is only granted to system apps"
errorLine1=' &lt;uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />'
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="12"
column="22"/>
</issue>

</issues>
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class AppNotificationListener : NotificationListenerService() {

fun isAppNotificationActive(packageName: String): Boolean {
if (!connected.get()) return false
return packageName in activeNotifications_
return activeNotifications_.containsKey(packageName)
}

fun isConnected(): Boolean = connected.get()
Expand Down
4 changes: 3 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ pluginManagement {
}
}

dependencyResolution {
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
Expand Down
Loading