@@ -26,7 +26,17 @@ enum class SecurityIssueType(val displayName: String) {
2626 EXPORTED_RECEIVER (" Exported Broadcast Receiver Without Permission" ),
2727 EXPORTED_PROVIDER (" Exported Content Provider Without Permission" ),
2828 // New: Intent Filter Security
29- INTENT_FILTER_DATA_EXPOSURE (" Intent Filter May Expose Data" )
29+ INTENT_FILTER_DATA_EXPOSURE (" Intent Filter May Expose Data" ),
30+ // New: Network Security
31+ MISSING_NETWORK_SECURITY_CONFIG (" Missing Network Security Config" ),
32+ CLEAR_TEXT_HTTP_URL (" Cleartext HTTP URL Found" ),
33+ NO_CERTIFICATE_PINNING (" Missing Certificate Pinning" ),
34+ INSECURE_HTTP_URL (" Insecure HTTP URL in Code" ),
35+ // New: ProGuard/R8 Analysis
36+ MISSING_PROGUARD_RULES (" Missing ProGuard/R8 Rules" ),
37+ NO_KEEP_CLASS_MEMBERS (" Missing -keepclassmembers Rules" ),
38+ NO_OBFUSCATION (" No Obfuscation Enabled" ),
39+ MISSING_LIBRARY_RULES (" Missing Rules for Libraries" )
3040}
3141
3242/* *
@@ -86,7 +96,7 @@ open class SecurityCheckTask : DefaultTask() {
8696 val content = buildFile.readText()
8797
8898 if (extension.get().checkDebuggable) {
89- val releaseDebugPattern = Regex (""" release\s*\{[^}]* debuggable\s*=\s*true""" , RegexOption .MULTILINE )
99+ val releaseDebugPattern = Regex (""" release\s*\{[\s\S]*? debuggable\s*=\s*true""" , RegexOption .MULTILINE )
90100 if (releaseDebugPattern.containsMatchIn(content)) {
91101 findings.add(
92102 SecurityFinding (
@@ -101,7 +111,7 @@ open class SecurityCheckTask : DefaultTask() {
101111 }
102112
103113 if (extension.get().checkMinifyEnabled) {
104- val releaseMinifyPattern = Regex (""" release\s*\{[^}]* minifyEnabled\s*=\s*false""" , RegexOption .MULTILINE )
114+ val releaseMinifyPattern = Regex (""" release\s*\{[\s\S]*? minifyEnabled\s*=\s*false""" , RegexOption .MULTILINE )
105115 if (releaseMinifyPattern.containsMatchIn(content)) {
106116 findings.add(
107117 SecurityFinding (
@@ -210,6 +220,10 @@ open class SecurityCheckTask : DefaultTask() {
210220 checkPermissions(content)
211221 checkComponentSecurity(content)
212222 checkIntentFilterSecurity(content)
223+ checkNetworkSecurity(content)
224+ checkHttpUrlsInCode()
225+ checkCertificatePinning()
226+ checkProGuardRules()
213227
214228 } catch (e: Exception ) {
215229 logger.warn(" Could not analyze manifest: ${e.message} " )
@@ -341,6 +355,232 @@ open class SecurityCheckTask : DefaultTask() {
341355 }
342356 }
343357
358+ private fun checkNetworkSecurity (content : String ) {
359+ // Check for Network Security Config
360+ val networkSecurityConfigPattern = """ android:networkSecurityConfig="(@+xml/|)network_security_config"""" .toRegex()
361+ val hasNetworkSecurityConfig = networkSecurityConfigPattern.containsMatchIn(content)
362+
363+ if (! hasNetworkSecurityConfig) {
364+ findings.add(
365+ SecurityFinding (
366+ type = SecurityIssueType .MISSING_NETWORK_SECURITY_CONFIG ,
367+ severity = Severity .MEDIUM ,
368+ message = " Missing Network Security Config - consider adding one to enforce HTTPS" ,
369+ location = " AndroidManifest.xml (<application>)" ,
370+ buildType = " all"
371+ )
372+ )
373+ }
374+
375+ // Check for cleartext traffic permission
376+ if (content.contains(" android:usesCleartextTraffic=\" true\" " )) {
377+ findings.add(
378+ SecurityFinding (
379+ type = SecurityIssueType .CLEAR_TEXT_HTTP_URL ,
380+ severity = Severity .MEDIUM ,
381+ message = " Cleartext traffic (HTTP) is allowed - this can be intercepted" ,
382+ location = " AndroidManifest.xml (<application>)" ,
383+ buildType = " all"
384+ )
385+ )
386+ }
387+ }
388+
389+ private fun checkHttpUrlsInCode () {
390+ // Scan source files for HTTP URLs
391+ val sourceDirs = listOf (
392+ project.file(" src/main/java" ),
393+ project.file(" src/main/kotlin" )
394+ )
395+
396+ val httpUrlPattern = Regex (""" https?://[^\s"'<>]+""" )
397+
398+ sourceDirs.forEach { dir ->
399+ if (dir.exists()) {
400+ dir.walkTopDown().filter { it.extension in listOf (" kt" , " java" , " xml" ) }.forEach { file ->
401+ try {
402+ val content = file.readText()
403+ httpUrlPattern.findAll(content).forEach { match ->
404+ val url = match.value
405+ if (url.startsWith(" http://" )) {
406+ findings.add(
407+ SecurityFinding (
408+ type = SecurityIssueType .INSECURE_HTTP_URL ,
409+ severity = Severity .MEDIUM ,
410+ message = " Insecure HTTP URL found: $url " ,
411+ location = " ${file.relativeTo(project.rootDir)} " ,
412+ buildType = " all"
413+ )
414+ )
415+ }
416+ }
417+ } catch (e: Exception ) {
418+ // Skip files that can't be read
419+ }
420+ }
421+ }
422+ }
423+ }
424+
425+ private fun checkCertificatePinning () {
426+ // Scan source files for certificate pinning implementation
427+ val sourceDirs = listOf (
428+ project.file(" src/main/java" ),
429+ project.file(" src/main/kotlin" )
430+ )
431+
432+ val pinningKeywords = listOf (
433+ " CertificatePinner" ,
434+ " pinCertificate" ,
435+ " setPinning" ,
436+ " validateCertificate"
437+ )
438+
439+ var hasPinning = false
440+
441+ sourceDirs.forEach { dir ->
442+ if (dir.exists()) {
443+ dir.walkTopDown().filter { it.extension in listOf (" kt" , " java" ) }.forEach { file ->
444+ try {
445+ val content = file.readText()
446+ if (pinningKeywords.any { content.contains(it, ignoreCase = true ) }) {
447+ hasPinning = true
448+ }
449+ } catch (e: Exception ) {
450+ // Skip files that can't be read
451+ }
452+ }
453+ }
454+ }
455+
456+ if (! hasPinning) {
457+ findings.add(
458+ SecurityFinding (
459+ type = SecurityIssueType .NO_CERTIFICATE_PINNING ,
460+ severity = Severity .LOW ,
461+ message = " No certificate pinning detected - consider adding for enhanced security" ,
462+ location = " Source code" ,
463+ buildType = " all"
464+ )
465+ )
466+ }
467+ }
468+
469+ private fun checkProGuardRules () {
470+ // Check if minify is enabled in build config
471+ val buildFile = project.file(" build.gradle" )
472+ val buildKtsFile = project.file(" build.gradle.kts" )
473+
474+ var minifyEnabled = false
475+
476+ // Check build.gradle.kts in current project (app module)
477+ if (buildKtsFile.exists()) {
478+ val content = buildKtsFile.readText()
479+ // Use [\s\S]*? to match across newlines (non-greedy)
480+ if (Regex (""" release\s*\{[\s\S]*?isMinifyEnabled\s*=\s*true""" , RegexOption .MULTILINE ).containsMatchIn(content)) {
481+ minifyEnabled = true
482+ }
483+ }
484+
485+ // Check build.gradle (Groovy) in current project
486+ if (buildFile.exists()) {
487+ val content = buildFile.readText()
488+ if (Regex (""" release\s*\{[\s\S]*?minifyEnabled\s*=\s*true""" , RegexOption .MULTILINE ).containsMatchIn(content)) {
489+ minifyEnabled = true
490+ }
491+ }
492+
493+ // Also check root project directory if we're in app module
494+ val rootBuildFile = project.file(" ../build.gradle" )
495+ val rootBuildKtsFile = project.file(" ../build.gradle.kts" )
496+
497+ if (rootBuildKtsFile.exists()) {
498+ val content = rootBuildKtsFile.readText()
499+ if (Regex (""" release\s*\{[\s\S]*?isMinifyEnabled\s*=\s*true""" , RegexOption .MULTILINE ).containsMatchIn(content)) {
500+ minifyEnabled = true
501+ }
502+ }
503+
504+ if (rootBuildFile.exists()) {
505+ val content = rootBuildFile.readText()
506+ if (Regex (""" release\s*\{[\s\S]*?minifyEnabled\s*=\s*true""" , RegexOption .MULTILINE ).containsMatchIn(content)) {
507+ minifyEnabled = true
508+ }
509+ }
510+
511+ if (minifyEnabled) {
512+ // Check for ProGuard rules file
513+ val proguardFiles = listOf (
514+ project.file(" proguard-rules.pro" ),
515+ project.file(" app/proguard-rules.pro" ),
516+ project.file(" ../proguard-rules.pro" ),
517+ project.file(" ../app/proguard-rules.pro" ),
518+ project.file(" proguard-android.txt" ),
519+ project.file(" app/proguard-android.txt" )
520+ )
521+
522+ val rulesFile = proguardFiles.firstOrNull { it.exists() }
523+
524+ if (rulesFile != null ) {
525+ val rulesContent = rulesFile.readText()
526+
527+ // Check for common library rules
528+ val commonLibraries = listOf (
529+ " okhttp" to " -dontwarn okhttp3" ,
530+ " retrofit" to " -dontwarn retrofit2" ,
531+ " gson" to " -keepattributes Signature" ,
532+ " rxjava" to " -dontwarn rxjava" ,
533+ " commons-io" to " -dontwarn org.apache.commons.io"
534+ )
535+
536+ commonLibraries.forEach { (library, rule) ->
537+ val hasLibrary = rulesContent.contains(library, ignoreCase = true )
538+ // Check if there's a ProGuard rule for this library
539+ // Look for lines starting with -dontwarn or -keep that contain the library name
540+ // Use \b (word boundary) to avoid matching "okhttp3" for library "okhttp"
541+ val proguardRulePattern = Regex (""" ^\s*-(dontwarn|keep|warn)\s+.*\b$library \b""" , RegexOption .MULTILINE )
542+ val hasRule = proguardRulePattern.containsMatchIn(rulesContent)
543+ if (hasLibrary && ! hasRule) {
544+ findings.add(
545+ SecurityFinding (
546+ type = SecurityIssueType .MISSING_LIBRARY_RULES ,
547+ severity = Severity .LOW ,
548+ message = " Library '$library ' may need additional rules" ,
549+ location = rulesFile.relativeTo(project.rootDir).path,
550+ buildType = " release"
551+ )
552+ )
553+ }
554+ }
555+
556+ // Check for -keepclassmembers rules (ignore comments, look for rule at start of line)
557+ val keepClassMembersPattern = Regex (""" ^\s*-keepclassmembers""" , RegexOption .MULTILINE )
558+ val hasKeepClassMembers = keepClassMembersPattern.containsMatchIn(rulesContent)
559+ if (! hasKeepClassMembers) {
560+ findings.add(
561+ SecurityFinding (
562+ type = SecurityIssueType .NO_KEEP_CLASS_MEMBERS ,
563+ severity = Severity .LOW ,
564+ message = " No -keepclassmembers rules found - consider adding for model classes" ,
565+ location = rulesFile.relativeTo(project.rootDir).path,
566+ buildType = " release"
567+ )
568+ )
569+ }
570+ } else {
571+ findings.add(
572+ SecurityFinding (
573+ type = SecurityIssueType .MISSING_PROGUARD_RULES ,
574+ severity = Severity .MEDIUM ,
575+ message = " ProGuard rules file not found - create one for better obfuscation" ,
576+ location = " proguard-rules.pro" ,
577+ buildType = " release"
578+ )
579+ )
580+ }
581+ }
582+ }
583+
344584 private fun logFindings () {
345585 logger.quiet(" =" .repeat(50 ))
346586 logger.quiet(" Security Check Results" )
0 commit comments