├── assets ├── openapi_example.png ├── rename_example.png └── mapping_example.txt ├── src └── main │ ├── java │ └── ru │ │ └── blackfan │ │ └── bfscan │ │ ├── parsing │ │ ├── httprequests │ │ │ ├── processors │ │ │ │ ├── ArgProcessingState.java │ │ │ │ ├── AnnotationProcessorFactory.java │ │ │ │ ├── AnnotationProcessor.java │ │ │ │ ├── CommonProcessor.java │ │ │ │ ├── GeneralTypesConstants.java │ │ │ │ ├── MinifiedProcessor.java │ │ │ │ ├── KtorResourceProcessor.java │ │ │ │ ├── Struts2Processor.java │ │ │ │ ├── FeignProcessor.java │ │ │ │ ├── FieldAnnotationProcessor.java │ │ │ │ ├── RetrofitProcessor.java │ │ │ │ ├── SwaggerProcessor.java │ │ │ │ ├── JaxJakartaProcessor.java │ │ │ │ ├── MicronautProcessor.java │ │ │ │ ├── SpringProcessor.java │ │ │ │ └── OpenApiProcessor.java │ │ │ ├── requestbody │ │ │ │ ├── RequestBody.java │ │ │ │ ├── RawRequestBody.java │ │ │ │ ├── PrimitiveRequestBody.java │ │ │ │ └── ObjectRequestBody.java │ │ │ ├── ParameterInfo.java │ │ │ ├── MultiHTTPRequest.java │ │ │ └── HTTPRequestProcessor.java │ │ └── constants │ │ │ ├── file │ │ │ ├── Properties.java │ │ │ ├── ApkResources.java │ │ │ ├── Xml.java │ │ │ ├── Yml.java │ │ │ └── Class.java │ │ │ └── ConstantsProcessor.java │ │ ├── config │ │ ├── ProcessorConfig.java │ │ └── ConfigLoader.java │ │ ├── cli │ │ └── CommandLineResult.java │ │ ├── jadx │ │ └── JadxBasePluginLoader.java │ │ ├── helpers │ │ ├── KeyValuePair.java │ │ └── Helpers.java │ │ └── Main.java │ └── resources │ ├── simplelogger.properties │ ├── META-INF │ └── services │ │ └── jadx.api.plugins.JadxPlugin │ └── application.properties ├── NOTICE ├── README.md ├── pom.xml └── LICENSE /assets/openapi_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackFan/BFScan/HEAD/assets/openapi_example.png -------------------------------------------------------------------------------- /assets/rename_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackFan/BFScan/HEAD/assets/rename_example.png -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/ArgProcessingState.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | public enum ArgProcessingState { 4 | NOT_PROCESSED, 5 | PROCESSED_NO_PARAMETER, 6 | PARAMETER_CREATED 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.defaultLogLevel=info 2 | org.slf4j.simpleLogger.log.jadx=off 3 | org.slf4j.simpleLogger.showDateTime=true 4 | org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss 5 | org.slf4j.simpleLogger.showThreadName=false 6 | org.slf4j.simpleLogger.showShortLogName=true -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin: -------------------------------------------------------------------------------- 1 | jadx.plugins.input.dex.DexInputPlugin 2 | jadx.plugins.input.java.JavaInputPlugin 3 | jadx.plugins.input.xapk.XapkInputPlugin 4 | jadx.plugins.input.apkm.ApkmInputPlugin 5 | jadx.plugins.kotlin.metadata.KotlinMetadataPlugin 6 | jadx.plugins.mappings.RenameMappingsPlugin -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/requestbody/RequestBody.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.requestbody; 2 | 3 | public interface RequestBody { 4 | 5 | public enum Type { PRIMITIVE, OBJECT, RAW }; 6 | 7 | public Type getType(); 8 | public Object getBody(); 9 | public void setBody(Object body); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /assets/mapping_example.txt: -------------------------------------------------------------------------------- 1 | a/b/aa -> retrofit2/http/HTTP: 2 | a/b/ab -> retrofit2/http/HEAD: 3 | a/b/ac -> retrofit2/http/GET: 4 | a/b/ad -> retrofit2/http/POST: 5 | a/b/ae -> retrofit2/http/PUT: 6 | a/b/af -> retrofit2/http/DELETE: 7 | a/b/ag -> retrofit2/http/PATCH: 8 | a/b/ah -> retrofit2/http/OPTIONS: 9 | a/b/ai -> retrofit2/http/Headers: 10 | a/b/aj -> retrofit2/http/Multipart: 11 | a/b/ak -> retrofit2/http/FormUrlEncoded: 12 | a/b/al -> retrofit2/http/Header: 13 | a/b/am -> retrofit2/http/Part: 14 | a/b/an -> retrofit2/http/Field: 15 | a/b/ao -> retrofit2/http/Body: 16 | a/b/ap -> retrofit2/http/Query: 17 | a/b/aq -> retrofit2/http/Path: -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/requestbody/RawRequestBody.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.requestbody; 2 | 3 | public class RawRequestBody implements RequestBody { 4 | 5 | String rawBody; 6 | 7 | public RawRequestBody(String body) { 8 | rawBody = body; 9 | } 10 | 11 | public RawRequestBody(RequestBody body) { 12 | rawBody = (String) body.getBody(); 13 | } 14 | 15 | @Override 16 | public Type getType() { 17 | return RequestBody.Type.RAW; 18 | } 19 | 20 | @Override 21 | public Object getBody() { 22 | return rawBody; 23 | } 24 | 25 | @Override 26 | public void setBody(Object body) { 27 | rawBody = (String) body; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/requestbody/PrimitiveRequestBody.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.requestbody; 2 | 3 | public class PrimitiveRequestBody implements RequestBody { 4 | 5 | Object primitiveBody; 6 | 7 | public PrimitiveRequestBody(Object body) { 8 | primitiveBody = body; 9 | } 10 | 11 | public PrimitiveRequestBody(PrimitiveRequestBody body) { 12 | primitiveBody = body.getBody(); 13 | } 14 | 15 | @Override 16 | public Type getType() { 17 | return Type.PRIMITIVE; 18 | } 19 | 20 | @Override 21 | public Object getBody() { 22 | return primitiveBody; 23 | } 24 | 25 | @Override 26 | public void setBody(Object body) { 27 | primitiveBody = body; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/config/ProcessorConfig.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.config; 2 | 3 | public class ProcessorConfig { 4 | private static ProcessorConfig instance; 5 | private boolean minifiedAnnotationsSupport; 6 | 7 | private ProcessorConfig() { 8 | this.minifiedAnnotationsSupport = false; 9 | } 10 | 11 | public static ProcessorConfig getInstance() { 12 | if (instance == null) { 13 | instance = new ProcessorConfig(); 14 | } 15 | return instance; 16 | } 17 | 18 | public boolean isMinifiedAnnotationsSupport() { 19 | return minifiedAnnotationsSupport; 20 | } 21 | 22 | public void setMinifiedAnnotationsSupport(boolean minifiedAnnotationsSupport) { 23 | this.minifiedAnnotationsSupport = minifiedAnnotationsSupport; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/constants/file/Properties.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.constants.file; 2 | 3 | import java.io.InputStream; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import ru.blackfan.bfscan.helpers.KeyValuePair; 8 | 9 | public class Properties { 10 | 11 | public static Set process(String fileName, InputStream is) throws Exception { 12 | Set keyValuePairs = new HashSet<>(); 13 | java.util.Properties properties = new java.util.Properties(); 14 | properties.load(is); 15 | for (Map.Entry entry : properties.entrySet()) { 16 | keyValuePairs.add(new KeyValuePair((String) entry.getKey(), (String) entry.getValue())); 17 | } 18 | return keyValuePairs; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/ParameterInfo.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests; 2 | 3 | public class ParameterInfo { 4 | 5 | private String defaultValue; 6 | private String name; 7 | 8 | public ParameterInfo() { 9 | defaultValue = ""; 10 | name = null; 11 | } 12 | 13 | public ParameterInfo(String name, String defaultValue) { 14 | this.defaultValue = defaultValue; 15 | this.name = name; 16 | } 17 | 18 | public String getName() { 19 | return name; 20 | } 21 | 22 | public void setName(String name) { 23 | this.name = name; 24 | } 25 | 26 | public String getDefaultValue() { 27 | return defaultValue; 28 | } 29 | 30 | public void setDefaultValue(String defaultValue) { 31 | this.defaultValue = defaultValue; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/cli/CommandLineResult.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.cli; 2 | 3 | import java.io.File; 4 | import java.net.URI; 5 | import java.util.List; 6 | 7 | public class CommandLineResult { 8 | 9 | public final List inputFiles; 10 | public final URI apiUrl; 11 | public final String searchString; 12 | public final String mode; 13 | public final String renameMappingFile; 14 | public final boolean minifiedAnnotationsSupport; 15 | 16 | public CommandLineResult(List inputFiles, URI apiUrl, String searchString, String mode, String renameMappingFile, boolean minifiedAnnotationsSupport) { 17 | this.inputFiles = inputFiles; 18 | this.apiUrl = apiUrl; 19 | this.searchString = searchString; 20 | this.mode = mode; 21 | this.renameMappingFile = renameMappingFile; 22 | this.minifiedAnnotationsSupport = minifiedAnnotationsSupport; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/AnnotationProcessorFactory.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class AnnotationProcessorFactory { 7 | private static final List processors = new ArrayList<>(); 8 | 9 | static { 10 | processors.add(new SwaggerProcessor()); 11 | processors.add(new SpringProcessor()); 12 | processors.add(new JaxJakartaProcessor()); 13 | processors.add(new MicronautProcessor()); 14 | processors.add(new FeignProcessor()); 15 | processors.add(new Struts2Processor()); 16 | processors.add(new RetrofitProcessor()); 17 | processors.add(new KtorResourceProcessor()); 18 | processors.add(new CommonProcessor()); 19 | processors.add(new OpenApiProcessor()); 20 | } 21 | 22 | public static List getProcessors() { 23 | return processors; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/jadx/JadxBasePluginLoader.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.jadx; 2 | 3 | import jadx.api.plugins.JadxPlugin; 4 | import jadx.api.plugins.loader.JadxPluginLoader; 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class JadxBasePluginLoader implements JadxPluginLoader { 10 | 11 | @Override 12 | public List load() { 13 | List list = new ArrayList<>(); 14 | list.add(new jadx.plugins.input.dex.DexInputPlugin()); 15 | list.add(new jadx.plugins.input.java.JavaInputPlugin()); 16 | list.add(new jadx.plugins.input.xapk.XApkInputPlugin()); 17 | list.add(new jadx.plugins.input.apkm.ApkmInputPlugin()); 18 | list.add(new jadx.plugins.kotlin.metadata.KotlinMetadataPlugin()); 19 | list.add(new jadx.plugins.mappings.RenameMappingsPlugin()); 20 | return list; 21 | } 22 | 23 | @Override 24 | public void close() throws IOException { 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/helpers/KeyValuePair.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.helpers; 2 | 3 | import java.util.Objects; 4 | 5 | public class KeyValuePair { 6 | 7 | private final String key; 8 | private final String value; 9 | 10 | public KeyValuePair(String key, String value) { 11 | this.key = key; 12 | this.value = value; 13 | } 14 | 15 | public String getKey() { 16 | return key; 17 | } 18 | 19 | public String getValue() { 20 | return value; 21 | } 22 | 23 | @Override 24 | public boolean equals(Object obj) { 25 | if (this == obj) { 26 | return true; 27 | } 28 | if (obj == null || getClass() != obj.getClass()) { 29 | return false; 30 | } 31 | KeyValuePair that = (KeyValuePair) obj; 32 | return Objects.equals(key, that.key) && Objects.equals(value, that.value); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(key, value); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/requestbody/ObjectRequestBody.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.requestbody; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class ObjectRequestBody implements RequestBody { 7 | public Map bodyParameters; 8 | 9 | public ObjectRequestBody(Map body) { 10 | bodyParameters = body; 11 | } 12 | 13 | public ObjectRequestBody(ObjectRequestBody body) { 14 | bodyParameters = new HashMap<>(body.bodyParameters); 15 | } 16 | 17 | public ObjectRequestBody() { 18 | bodyParameters = new HashMap<>(); 19 | } 20 | 21 | public void putBodyParameter(String key, Object value) { 22 | bodyParameters.put(key, value); 23 | } 24 | 25 | public void putBodyParameters(Map parameters) { 26 | bodyParameters.putAll(parameters); 27 | } 28 | 29 | @Override 30 | public Type getType() { 31 | return Type.OBJECT; 32 | } 33 | 34 | @Override 35 | public Object getBody() { 36 | return bodyParameters; 37 | } 38 | 39 | @Override 40 | public void setBody(Object body) { 41 | bodyParameters = (HashMap) body; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | excluded.packages=android.,androidx.,com.google.,com.android.,okhttp3.,org.apache.,\ 2 | org.spongycastle.,org.bouncycastle.,com.mysql.,com.sun.,javax.,org.yaml.,org.slf4j.,\ 3 | liquibase.,org.codehaus.,org.eclipse.,org.springframework.,org.openxmlformats.,\ 4 | org.hibernate.,gnu.crypto.,groovy.,net.sf.,com.omg.,org.omg.,org.tritonus.,com.ibm.,\ 5 | com.fasterxml.,org.jboss.,org.jdom2.,org.mariadb.,org.postgresql.,org.mozilla.,\ 6 | org.jfree.,org.xmlpull.,org.hsqldb.,ch.qos.,com.facebook.,scala.,antlr.,\ 7 | org.xhtmlrenderer.,org.glassfish.,org.aspectj.,com.lowagie.,com.microsoft.,\ 8 | jas.,oracle.,org.w3.,net.lingala,com.zaxxer.,ch.qos.,kotlin.,kotlinx.,okio.,io.ktor.\ 9 | io.appmetrica.,com.appsflyer 10 | 11 | excluded.secretsRegexp=^((ru|com|org|net|oracle|javax|java|android|androidx|kotlin|WEB-INF|META-INF|@(anim|bool|drawable|\\+id|color|dimen|style|string|layout|integer|android:color|android:style|interpolator|font|fraction|xml)|\\?(attr|android:attr)|!(text|icon))(\\.|/)|SMAP\n).*|.*(\\.kt|\\(\\.\\.\\.\\))$ 12 | 13 | excluded.linksRegexp=^https?://(www\\.w3\\.org|(maven|svn|xml|www|commons|xmlbeans|logging|)\\.?apache\\.org|(java|www)\\.sun\\.com|www\\.springframework\\.org|(schemas|developer)\\.android\\.com|www\\.slf4j\\.org|xml\\.org|docs\\.oracle\\.com|www\\.eclipse\\.org|www\\.bouncycastle\\.org|schemas\\.xmlsoap\\.org|schemas\\.microsoft\\.com|schemas\\.openxmlformats\\.org|ns\\.adobe\\.com)/.* -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/AnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.ILocalVar; 5 | import jadx.core.dex.instructions.args.ArgType; 6 | import jadx.core.dex.nodes.RootNode; 7 | import java.util.List; 8 | import java.util.Map; 9 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 10 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 11 | 12 | public interface AnnotationProcessor { 13 | 14 | ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 15 | ParameterInfo paramInfo, 16 | String annotationClass, 17 | Map annotationValues, 18 | List localVars, 19 | int regNum, 20 | ArgType var, 21 | RootNode rn) throws Exception; 22 | 23 | boolean processMethodAnnotations(MultiHTTPRequest request, 24 | String annotationClass, 25 | Map annotationValues, 26 | RootNode rn) 27 | throws Exception; 28 | 29 | boolean processClassAnnotations(MultiHTTPRequest request, 30 | String annotationClass, 31 | Map annotationValues, 32 | String globalBasePath, 33 | String className, 34 | RootNode rn); 35 | } 36 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Sergey Bobrov (BlackFan) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | *********************************************************************** 17 | 18 | This project includes third-party libraries distributed under various licenses: 19 | 20 | jadx Copyright 2015, Skylot 21 | License: Apache License, Version 2.0 22 | Source: https://github.com/skylot/jadx/ 23 | 24 | 25 | swagger-core Copyright 2015, SmartBear Software Inc 26 | License: Apache License, Version 2.0 27 | Source: https://github.com/swagger-api/swagger-core 28 | 29 | 30 | gson Copyright 2008, Google Inc. 31 | License: Apache License, Version 2.0 32 | Source: https://github.com/google/gson 33 | 34 | 35 | Apache Commons CLI Copyright 2002-2025, The Apache Software Foundation 36 | License: Apache License, Version 2.0 37 | Source: https://github.com/apache/commons-cli 38 | 39 | 40 | snakeyaml Copyright 2008, SnakeYAML 41 | License: Apache License, Version 2.0 42 | Source: https://github.com/snakeyaml/snakeyaml 43 | 44 | 45 | slf4j-simple Copyright 2004-2022, QOS.ch Sarl (Switzerland) 46 | License: MIT License 47 | Source: https://github.com/qos-ch/slf4j -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/CommonProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.ILocalVar; 5 | import jadx.core.dex.instructions.args.ArgType; 6 | import jadx.core.dex.nodes.RootNode; 7 | import java.util.List; 8 | import java.util.Map; 9 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 10 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 11 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 12 | 13 | public class CommonProcessor implements AnnotationProcessor { 14 | 15 | @Override 16 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 17 | ParameterInfo paramInfo, 18 | String annotationClass, 19 | Map annotationValues, 20 | List localVars, 21 | int methodArg, 22 | ArgType var, 23 | RootNode rn) { 24 | if(Constants.Ignore.IGNORE_ANNOTATIONS.contains(annotationClass)) { 25 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 26 | } else { 27 | return ArgProcessingState.NOT_PROCESSED; 28 | } 29 | } 30 | 31 | @Override 32 | public boolean processMethodAnnotations(MultiHTTPRequest request, 33 | String annotationClass, 34 | Map annotationValues, 35 | RootNode rn) { 36 | return Constants.Serialization.SERIALIZATION_ANNOTATIONS.contains(annotationClass); 37 | } 38 | 39 | @Override 40 | public boolean processClassAnnotations(MultiHTTPRequest request, 41 | String annotationClass, 42 | Map annotationValues, 43 | String globalBasePath, 44 | String className, 45 | RootNode rn 46 | ) { 47 | return false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/config/ConfigLoader.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.config; 2 | 3 | import java.io.InputStream; 4 | import java.io.IOException; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.Properties; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class ConfigLoader { 12 | private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class); 13 | private static final String PROPERTIES_FILE = "application.properties"; 14 | private static List excludedPackages; 15 | private static String excludedSecretsRegexp; 16 | private static String excludedLinksRegexp; 17 | 18 | static { 19 | try (InputStream input = ConfigLoader.class.getClassLoader().getResourceAsStream(PROPERTIES_FILE)) { 20 | if (input == null) { 21 | logger.error("Configuration file not found: " + PROPERTIES_FILE); 22 | excludedPackages = Arrays.asList(""); 23 | excludedSecretsRegexp = ""; 24 | excludedLinksRegexp = ""; 25 | } else { 26 | Properties properties = new Properties(); 27 | properties.load(input); 28 | String packages = properties.getProperty("excluded.packages", ""); 29 | excludedPackages = Arrays.asList(packages.split(",")); 30 | excludedSecretsRegexp = properties.getProperty("excluded.secretsRegexp", ""); 31 | excludedLinksRegexp = properties.getProperty("excluded.linksRegexp", ""); 32 | } 33 | } catch (IOException e) { 34 | logger.error("Error loading configuration file", e); 35 | } 36 | } 37 | 38 | public static List getExcludedPackages() { 39 | return excludedPackages; 40 | } 41 | 42 | public static String getExcludedSecretsRegexp() { 43 | return excludedSecretsRegexp; 44 | } 45 | 46 | public static String getExcludedLinksRegexp() { 47 | return excludedLinksRegexp; 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/constants/file/ApkResources.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.constants.file; 2 | 3 | import jadx.core.xmlgen.ResContainer; 4 | import java.io.StringReader; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | import javax.xml.parsers.DocumentBuilder; 8 | import javax.xml.parsers.DocumentBuilderFactory; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.w3c.dom.Document; 12 | import org.w3c.dom.Element; 13 | import org.w3c.dom.NodeList; 14 | import org.xml.sax.InputSource; 15 | import ru.blackfan.bfscan.helpers.KeyValuePair; 16 | 17 | public class ApkResources { 18 | private static final Logger logger = LoggerFactory.getLogger(ApkResources.class); 19 | 20 | public static Set process(ResContainer resContainer, DocumentBuilderFactory factory) { 21 | Set keyValuePairs = new HashSet<>(); 22 | try { 23 | if (resContainer.getDataType() == ResContainer.DataType.RES_TABLE) { 24 | for (ResContainer subFile : resContainer.getSubFiles()) { 25 | if (subFile.getDataType() == ResContainer.DataType.TEXT) { 26 | String fileName = subFile.getFileName(); 27 | String xmlContent = subFile.getText().toString(); 28 | DocumentBuilder builder = factory.newDocumentBuilder(); 29 | Document doc = builder.parse(new InputSource(new StringReader(xmlContent))); 30 | NodeList nodes = doc.getElementsByTagName("string"); 31 | for (int i = 0; i < nodes.getLength(); i++) { 32 | Element element = (Element) nodes.item(i); 33 | keyValuePairs.add(new KeyValuePair(fileName + "#" + element.getAttribute("name"), element.getTextContent())); 34 | } 35 | } 36 | } 37 | } 38 | } catch (Exception ex) { 39 | logger.error("Failed to process APK resource file", ex); 40 | } 41 | 42 | return keyValuePairs; 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/GeneralTypesConstants.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | public class GeneralTypesConstants { 8 | 9 | public static final String JAVA_BOOLEAN = "java.lang.Boolean"; 10 | public static final String JAVA_MAP = "java.util.Map"; 11 | public static final String JAVA_OPTIONAL = "java.util.Optional"; 12 | public static final String JAVA_STRING = "java.lang.String"; 13 | 14 | public static final Set MULTIPART_CLASSES = new HashSet<>(Arrays.asList( 15 | "org.springframework.web.multipart.MultipartFile", 16 | "javax.servlet.http.Part", 17 | "jakarta.servlet.http.Part", 18 | "io.micronaut.http.multipart.MultipartFile" 19 | )); 20 | 21 | public static final Set JSON_OBJECT_CLASSES = new HashSet<>(Arrays.asList( 22 | "org.json.JSONObject", 23 | "com.google.gson.JsonObject", 24 | "com.fasterxml.jackson.databind.JsonNode", 25 | "com.fasterxml.jackson.databind.ObjectNode" 26 | )); 27 | 28 | public static final Set JSON_ARRAY_CLASSES = new HashSet<>(Arrays.asList( 29 | "org.json.JSONArray", 30 | "com.google.gson.JsonArray", 31 | "com.fasterxml.jackson.databind.ArrayNode" 32 | )); 33 | 34 | public static final Set NUMBER_CLASSES = new HashSet<>(Arrays.asList( 35 | "java.lang.Byte", 36 | "java.lang.Short", 37 | "java.lang.Integer", 38 | "java.lang.Long", 39 | "java.lang.Float", 40 | "java.lang.Double", 41 | "java.math.BigInteger", 42 | "java.math.BigDecimal" 43 | )); 44 | 45 | public static final Set ARRAY_CLASSES = new HashSet<>(Arrays.asList( 46 | "java.util.List", 47 | "java.util.Set", 48 | "java.util.Collection", 49 | "java.util.ArrayList", 50 | "java.util.HashSet", 51 | "java.util.LinkedList", 52 | "java.util.TreeSet", 53 | "java.util.EnumSet" 54 | )); 55 | 56 | public static final Set DATE_CLASSES = new HashSet<>(Arrays.asList( 57 | "java.util.Date", 58 | "java.time.LocalDate", 59 | "java.time.ZonedDateTime" 60 | )); 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/MinifiedProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedType; 4 | import jadx.api.plugins.input.data.annotations.EncodedValue; 5 | import jadx.api.plugins.input.data.ILocalVar; 6 | import jadx.core.dex.instructions.args.ArgType; 7 | import jadx.core.dex.nodes.RootNode; 8 | import java.util.List; 9 | import java.util.Map; 10 | import ru.blackfan.bfscan.helpers.Helpers; 11 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 12 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 13 | 14 | public class MinifiedProcessor { 15 | 16 | public static ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 17 | ParameterInfo paramInfo, 18 | String annotationClass, 19 | Map annotationValues, 20 | List localVars, 21 | int methodArg, 22 | ArgType argType, 23 | RootNode rootNode) { 24 | 25 | EncodedValue name = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 26 | if (name != null && name.getType() == EncodedType.ENCODED_STRING) { 27 | String paramName = AnnotationUtils.getParamName(name, paramInfo, localVars, methodArg); 28 | if(Helpers.isValidRequestHeader(paramName)) { 29 | AnnotationUtils.processHeader(request, paramName, paramInfo.getDefaultValue()); 30 | return ArgProcessingState.PARAMETER_CREATED; 31 | } else { 32 | AnnotationUtils.processQueryParameter(request, paramName, paramInfo.getDefaultValue(), argType, rootNode); 33 | return ArgProcessingState.PARAMETER_CREATED; 34 | } 35 | } 36 | return ArgProcessingState.NOT_PROCESSED; 37 | } 38 | 39 | public static boolean processMethodAnnotations(MultiHTTPRequest request, 40 | String annotationClass, 41 | Map annotationValues, 42 | RootNode rn) { 43 | 44 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("path", "value", "a")); 45 | if (value != null) { 46 | String path = Helpers.stringWrapper(value); 47 | if (Helpers.isValidUrlPath(path) 48 | || (Helpers.isValidUrlPath("/" + path) && path.contains("/"))) { 49 | request.addAdditionalInformation("Minified annotations (experimental support)"); 50 | request.setPath(Helpers.stringWrapper(value), true); 51 | return true; 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/constants/file/Xml.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.constants.file; 2 | 3 | import java.io.InputStream; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | import javax.xml.parsers.DocumentBuilder; 7 | import javax.xml.parsers.DocumentBuilderFactory; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.w3c.dom.Document; 11 | import org.w3c.dom.Element; 12 | import org.w3c.dom.NamedNodeMap; 13 | import org.w3c.dom.Node; 14 | import org.w3c.dom.NodeList; 15 | import org.xml.sax.InputSource; 16 | import ru.blackfan.bfscan.helpers.KeyValuePair; 17 | 18 | public class Xml { 19 | private static final Logger logger = LoggerFactory.getLogger(Xml.class); 20 | 21 | public static Set process(String fileName, InputStream xmlContent, DocumentBuilderFactory factory) throws Exception { 22 | Set keyValuePairs = new HashSet<>(); 23 | try { 24 | DocumentBuilder builder = factory.newDocumentBuilder(); 25 | InputSource inputSource = new InputSource(xmlContent); 26 | Document document = builder.parse(inputSource); 27 | processElement(document.getDocumentElement(), keyValuePairs); 28 | } catch (Exception e) { 29 | logger.error("Error processing XML file {}: {}", fileName, e.getMessage()); 30 | throw e; 31 | } 32 | return keyValuePairs; 33 | } 34 | 35 | private static void processElement(Element element, Set keyValuePairs) { 36 | String nodeName = element.getNodeName(); 37 | 38 | String directText = getDirectTextContent(element).trim(); 39 | if (!directText.isEmpty()) { 40 | keyValuePairs.add(new KeyValuePair(nodeName, directText)); 41 | } 42 | 43 | NamedNodeMap attributes = element.getAttributes(); 44 | for (int i = 0; i < attributes.getLength(); i++) { 45 | Node attr = attributes.item(i); 46 | keyValuePairs.add(new KeyValuePair(nodeName + "@" + attr.getNodeName(), attr.getNodeValue())); 47 | } 48 | 49 | if(attributes.getNamedItem("name") != null && attributes.getNamedItem("value") != null) { 50 | keyValuePairs.add(new KeyValuePair(nodeName + "@" + attributes.getNamedItem("name").getNodeValue(), attributes.getNamedItem("value").getNodeValue())); 51 | } 52 | 53 | NodeList children = element.getChildNodes(); 54 | for (int i = 0; i < children.getLength(); i++) { 55 | Node child = children.item(i); 56 | if (child.getNodeType() == Node.ELEMENT_NODE) { 57 | processElement((Element) child, keyValuePairs); 58 | } 59 | } 60 | } 61 | 62 | private static String getDirectTextContent(Element element) { 63 | StringBuilder textContent = new StringBuilder(); 64 | NodeList children = element.getChildNodes(); 65 | for (int i = 0; i < children.getLength(); i++) { 66 | Node child = children.item(i); 67 | if (child.getNodeType() == Node.TEXT_NODE) { 68 | textContent.append(child.getNodeValue()); 69 | } 70 | } 71 | return textContent.toString(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/constants/file/Yml.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.constants.file; 2 | 3 | import java.io.InputStream; 4 | import java.util.Collection; 5 | import java.util.HashSet; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.yaml.snakeyaml.constructor.SafeConstructor; 11 | import org.yaml.snakeyaml.LoaderOptions; 12 | import org.yaml.snakeyaml.Yaml; 13 | import ru.blackfan.bfscan.helpers.KeyValuePair; 14 | 15 | public class Yml { 16 | private static final Logger logger = LoggerFactory.getLogger(Yml.class); 17 | private static final int MAX_DEPTH = 100; 18 | 19 | public static Set process(String fileName, InputStream is) throws Exception { 20 | Set keyValuePairs = new HashSet<>(); 21 | 22 | LoaderOptions options = new LoaderOptions(); 23 | options.setAllowDuplicateKeys(false); 24 | options.setMaxAliasesForCollections(100); 25 | options.setCodePointLimit(5 * 1024 * 1024); 26 | 27 | Yaml yaml = new Yaml(new SafeConstructor(options)); 28 | 29 | try { 30 | Iterable yamlDocuments = yaml.loadAll(is); 31 | for (Object yamlData : yamlDocuments) { 32 | if (yamlData instanceof Map) { 33 | parseYamlData("", (Map) yamlData, keyValuePairs, 0); 34 | } else if (yamlData != null) { 35 | logger.warn("Root YAML element in {} is not a map: {}", fileName, yamlData.getClass()); 36 | } 37 | } 38 | } catch (Exception e) { 39 | logger.error("Error parsing YAML file: " + fileName, e); 40 | throw e; 41 | } 42 | 43 | return keyValuePairs; 44 | } 45 | 46 | private static void parseYamlData(String prefix, Map data, Set keyValuePairs, int depth) { 47 | if (depth > MAX_DEPTH) { 48 | logger.warn("Maximum YAML parsing depth ({}) exceeded", MAX_DEPTH); 49 | return; 50 | } 51 | 52 | for (Map.Entry entry : data.entrySet()) { 53 | if (entry.getKey() == null) { 54 | continue; 55 | } 56 | 57 | String key = prefix.isEmpty() ? String.valueOf(entry.getKey()) : prefix + "." + entry.getKey(); 58 | Object value = entry.getValue(); 59 | 60 | if (value instanceof Map) { 61 | parseYamlData(key, (Map) value, keyValuePairs, depth + 1); 62 | } else if (value instanceof Collection) { 63 | parseCollection(key, (Collection) value, keyValuePairs, depth + 1); 64 | } else if (value != null) { 65 | keyValuePairs.add(new KeyValuePair(key, String.valueOf(value))); 66 | } 67 | } 68 | } 69 | 70 | private static void parseCollection(String prefix, Collection collection, Set keyValuePairs, int depth) { 71 | if (depth > MAX_DEPTH) { 72 | return; 73 | } 74 | 75 | int index = 0; 76 | for (Object item : collection) { 77 | String key = prefix + "[" + index + "]"; 78 | if (item instanceof Map) { 79 | parseYamlData(key, (Map) item, keyValuePairs, depth + 1); 80 | } else if (item instanceof Collection) { 81 | parseCollection(key, (Collection) item, keyValuePairs, depth + 1); 82 | } else if (item != null) { 83 | keyValuePairs.add(new KeyValuePair(key, String.valueOf(item))); 84 | } 85 | index++; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/KtorResourceProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.ILocalVar; 5 | import jadx.core.dex.instructions.args.ArgType; 6 | import jadx.core.dex.nodes.RootNode; 7 | import java.util.List; 8 | import java.util.Map; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import ru.blackfan.bfscan.helpers.Helpers; 12 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 13 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 14 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 15 | 16 | public class KtorResourceProcessor implements AnnotationProcessor { 17 | 18 | private static final Logger logger = LoggerFactory.getLogger(KtorResourceProcessor.class); 19 | 20 | @Override 21 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 22 | ParameterInfo paramInfo, 23 | String annotationClass, 24 | Map annotationValues, 25 | List localVars, 26 | int methodArg, 27 | ArgType argType, 28 | RootNode rootNode) throws Exception { 29 | return ArgProcessingState.NOT_PROCESSED; 30 | } 31 | 32 | @Override 33 | public boolean processMethodAnnotations(MultiHTTPRequest request, 34 | String annotationClass, 35 | Map annotationValues, 36 | RootNode rn) { 37 | switch (annotationClass) { 38 | case Constants.Ktor.RESOURCE, Constants.Ktor.RESOURCE2, Constants.Ktor.LOCATION -> { 39 | request.addAdditionalInformation("Ktor Resource"); 40 | 41 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("path")); 42 | if (value != null) { 43 | String path = Helpers.stringWrapper(value); 44 | request.setPath(path, false); 45 | return true; 46 | } 47 | return false; 48 | } 49 | default -> { 50 | return false; 51 | } 52 | } 53 | } 54 | 55 | @Override 56 | public boolean processClassAnnotations(MultiHTTPRequest request, 57 | String annotationClass, 58 | Map annotationValues, 59 | String globalBasePath, 60 | String className, 61 | RootNode rn) { 62 | switch (annotationClass) { 63 | case Constants.Ktor.RESOURCE, Constants.Ktor.RESOURCE2, Constants.Ktor.LOCATION -> { 64 | request.addAdditionalInformation("Ktor Resource"); 65 | 66 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("path")); 67 | if (value != null) { 68 | String classPath = Helpers.stringWrapper(value); 69 | String fullPath = (classPath.startsWith("/") 70 | ? globalBasePath.substring(0, globalBasePath.length() - 1) 71 | : globalBasePath) + classPath; 72 | request.setBasePaths(List.of(fullPath)); 73 | request.setPath("", false); 74 | try { 75 | Map parameters = AnnotationUtils.classToRequestParameters(Helpers.loadClass(rn, className), false, rn); 76 | AnnotationUtils.appendParametersToRequest(request, parameters); 77 | } catch (Exception e) { 78 | logger.error("Error parsing parameters for Ktor Resource", e); 79 | } 80 | return true; 81 | } 82 | return false; 83 | } 84 | default -> { 85 | return false; 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## BFScan 2 | 3 | Tool for initial processing of APK / XAPK / APKM / DEX / JAR / WAR applications. 4 | 5 | * Search for strings in source code and resources that look like URIs, paths, or secrets 6 | * Generate raw HTTP requests and OpenAPI specifications based on config files, class and method annotations 7 | * Supported client libraries 8 | * [Square Retrofit](https://square.github.io/retrofit/) 9 | * [Ktorfit](https://foso.github.io/Ktorfit/) 10 | * [Feign](https://github.com/OpenFeign/feign) 11 | * Supported server libraries 12 | * [Spring Web Annotations](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/package-summary.html) 13 | * [Spring SimpleUrlHandlerMapping](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/handler/SimpleUrlHandlerMapping.html) 14 | * [Spring BeanNameUrlHandlerMapping](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.html) 15 | * [JAX-RS](https://docs.oracle.com/javaee/7/api/javax/ws/rs/package-summary.html), [Jakarta RESTful Web Services](https://jakarta.ee/learn/specification-guides/restful-web-services-explained/) 16 | * [JAX](https://docs.oracle.com/javaee/7/api/javax/servlet/annotation/package-summary.html), [Jakarta Servlet Annotations](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/servlet/annotation/package-summary) 17 | * [Micronaut](https://docs.micronaut.io/latest/guide/#httpServer) 18 | * [web.xml Servlets](https://docs.oracle.com/cd/E24329_01/web.1211/e21049/web_xml.htm) 19 | * [jetty.xml Servlets](https://jetty.org/docs/jetty/12/programming-guide/server/http.html) 20 | * [Struts 1](https://weblegacy.github.io/struts1/) & [Struts 2 Actions](https://struts.apache.org/getting-started/coding-actions) 21 | * [Swagger](https://docs.swagger.io/swagger-core/v1.5.0/apidocs/) & [OpenApi Annotations](https://docs.swagger.io/swagger-core/v2.2.28/apidocs/) 22 | * [Ktor Type-safe routing](https://ktor.io/docs/server-resources.html) 23 | * [Play Framework routing](https://www.playframework.com/documentation/3.0.x/JavaRouting) 24 | 25 | ### Usage 26 | 27 | ```text 28 | java -jar bfscan.jar <...> [-m ] [-ma ] [-r ] [-s ] [-u ] [-v ] 29 | 30 | -m Mode ([a]ll, [s]ecrets, [h]ttp), default: all 31 | -ma Minified or unknown annotations support (yes, no), default: yes 32 | -r Deobfuscation mapping file 33 | -s Search string 34 | -u API base url (http://localhost/api/) 35 | -v Log level (off, error, warn, info, debug, trace) 36 | ``` 37 | 38 | ```text 39 | java -jar bfscan.jar test/*.apk -u https://example.tld/api/ 40 | ``` 41 | 42 | ### Example 43 | 44 | For this class using Spring annotations, the following results will be generated. 45 | ```java 46 | @RestController 47 | @RequestMapping("/api") 48 | public class UserController { 49 | 50 | @PostMapping("createUser") 51 | public String create(@RequestParam Optional someParamName, @RequestBody User user) { 52 | return "response"; 53 | } 54 | ``` 55 | 56 | #### Results 57 | 58 | **Method**: com.mycompany.springbootexample.UserController->create 59 | 60 | * Spring Method 61 | ``` 62 | POST /api/createUser?someParamName=value HTTP/1.1 63 | Host: localhost 64 | Connection: close 65 | Content-Type: application/json 66 | 67 | { 68 | "name": "name", 69 | "age": 1 70 | } 71 | ``` 72 | 73 | ![OpenAPI example](./assets/openapi_example.png) 74 | 75 | ### Obfuscated code 76 | 77 | If you are analyzing an application that uses a supported library, but its code is obfuscated, you can create a mapping file to replace the class names. 78 | 79 | To do this, you can manually use the "Rename" function in the decompiled code in the [jadx-gui](https://github.com/skylot/jadx), and then save the generated mapping file (File > Save mappings). Or use an [example mapping file](./assets/mapping_example.txt) for Retrofit and modify it according to your application. 80 | 81 | ![Class rename example](./assets/rename_example.png) 82 | 83 | Example of analyzing an application with renaming classes using a mapping file. 84 | ``` 85 | java -jar BFScan.jar ./tests/example.apk -r ./tests/mapping.txt 86 | ``` 87 | 88 | ### Acknowledgements 89 | This project uses: 90 | - [jadx](https://github.com/skylot/jadx) - Apache License 2.0 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | ru.blackfan 5 | BFScan 6 | 3.1.4 7 | jar 8 | 9 | 10 | google 11 | google 12 | https://maven.google.com 13 | 14 | 15 | 16 | 17 | 18 | org.yaml 19 | snakeyaml 20 | 2.4 21 | 22 | 23 | io.github.skylot 24 | jadx-input-api 25 | 1.5.3 26 | 27 | 28 | io.github.skylot 29 | jadx-dex-input 30 | 1.5.3 31 | 32 | 33 | io.github.skylot 34 | jadx-java-input 35 | 1.5.3 36 | 37 | 38 | io.github.skylot 39 | jadx-xapk-input 40 | 1.5.3 41 | 42 | 43 | io.github.skylot 44 | jadx-apkm-input 45 | 1.5.3 46 | 47 | 48 | io.github.skylot 49 | jadx-core 50 | 1.5.3 51 | 52 | 53 | io.github.skylot 54 | jadx-kotlin-metadata 55 | 1.5.3 56 | 57 | 58 | io.github.skylot 59 | jadx-rename-mappings 60 | 1.5.3 61 | 62 | 63 | org.slf4j 64 | slf4j-simple 65 | 2.0.17 66 | 67 | 68 | commons-cli 69 | commons-cli 70 | 1.9.0 71 | 72 | 73 | com.google.code.gson 74 | gson 75 | 2.12.1 76 | 77 | 78 | io.swagger.core.v3 79 | swagger-core 80 | 2.2.28 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-surefire-plugin 89 | 3.5.2 90 | 91 | 92 | maven-jar-plugin 93 | 3.4.2 94 | 95 | 96 | default-jar 97 | none 98 | 99 | 100 | 101 | 102 | maven-assembly-plugin 103 | 104 | ${project.artifactId}-${project.version} 105 | false 106 | 107 | jar-with-dependencies 108 | 109 | 110 | 111 | ru.blackfan.bfscan.Main 112 | 113 | 114 | 115 | 116 | 117 | make-assembly 118 | package 119 | 120 | single 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | UTF-8 130 | 17 131 | 17 132 | 133 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/Struts2Processor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.annotations.IAnnotation; 5 | import jadx.api.plugins.input.data.ILocalVar; 6 | import jadx.core.dex.instructions.args.ArgType; 7 | import jadx.core.dex.nodes.RootNode; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Map; 11 | import ru.blackfan.bfscan.helpers.Helpers; 12 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 13 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 14 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 15 | 16 | public class Struts2Processor implements AnnotationProcessor { 17 | 18 | @Override 19 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 20 | ParameterInfo paramInfo, 21 | String annotationClass, 22 | Map annotationValues, 23 | List localVars, 24 | int methodArg, 25 | ArgType var, 26 | RootNode rn) { 27 | return ArgProcessingState.NOT_PROCESSED; 28 | } 29 | 30 | @Override 31 | public boolean processMethodAnnotations(MultiHTTPRequest request, 32 | String annotationClass, 33 | Map annotationValues, 34 | RootNode rn) { 35 | switch (annotationClass) { 36 | case Constants.Struts.ACTION -> { 37 | processAction(request, annotationValues); 38 | return true; 39 | } 40 | case Constants.Struts.ACTIONS -> { 41 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 42 | if (value != null) { 43 | ArrayList pathList = (ArrayList) value.getValue(); 44 | List paths = new ArrayList(); 45 | for (EncodedValue path : pathList) { 46 | IAnnotation actionAnnotation = (IAnnotation) path.getValue(); 47 | if (actionAnnotation != null) { 48 | EncodedValue actionPath = AnnotationUtils.getValue(actionAnnotation.getValues(), List.of("value")); 49 | paths.add(Helpers.stringWrapper(actionPath)); 50 | } 51 | } 52 | request.addAdditionalInformation("Struts2 Action"); 53 | request.setPaths(paths); 54 | } 55 | return true; 56 | } 57 | default -> { 58 | return false; 59 | } 60 | } 61 | } 62 | 63 | private void processAction(MultiHTTPRequest request, Map annotationValues) { 64 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 65 | if (value != null) { 66 | request.addAdditionalInformation("Struts2 Action"); 67 | request.setPath(Helpers.stringWrapper(value), false); 68 | } 69 | } 70 | 71 | @Override 72 | public boolean processClassAnnotations(MultiHTTPRequest request, 73 | String annotationClass, 74 | Map annotationValues, 75 | String globalBasePath, 76 | String className, 77 | RootNode rn 78 | ) { 79 | switch (annotationClass) { 80 | case Constants.Struts.NAMESPACE -> { 81 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "path")); 82 | if (value != null) { 83 | String classPath = Helpers.stringWrapper(value); 84 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 85 | request.setBasePaths(List.of(fullPath)); 86 | } 87 | return false; 88 | } 89 | case Constants.Struts.NAMESPACES -> { 90 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 91 | if (value != null) { 92 | ArrayList pathList = (ArrayList) value.getValue(); 93 | List paths = new ArrayList(); 94 | for (EncodedValue path : pathList) { 95 | IAnnotation namespaceAnnotation = (IAnnotation) path.getValue(); 96 | if (namespaceAnnotation != null) { 97 | EncodedValue namespacePath = AnnotationUtils.getValue(namespaceAnnotation.getValues(), List.of("value")); 98 | paths.add(Helpers.stringWrapper(namespacePath)); 99 | } 100 | } 101 | request.setBasePaths(paths); 102 | } 103 | return false; 104 | } 105 | case Constants.Struts.ACTION -> { 106 | processAction(request, annotationValues); 107 | return true; 108 | } 109 | case Constants.Struts.ACTIONS -> { 110 | request.addAdditionalInformation("Struts2 Action"); 111 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 112 | if (value != null) { 113 | ArrayList pathList = (ArrayList) value.getValue(); 114 | List paths = new ArrayList(); 115 | for (EncodedValue path : pathList) { 116 | IAnnotation actionAnnotation = (IAnnotation) path.getValue(); 117 | if (actionAnnotation != null) { 118 | EncodedValue actionPath = AnnotationUtils.getValue(actionAnnotation.getValues(), List.of("value")); 119 | paths.add(Helpers.stringWrapper(actionPath)); 120 | } 121 | } 122 | request.setPaths(paths); 123 | } 124 | return true; 125 | } 126 | 127 | default -> { 128 | return false; 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/constants/file/Class.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.constants.file; 2 | 3 | import jadx.api.JavaClass; 4 | import jadx.api.JavaField; 5 | import jadx.api.JavaMethod; 6 | import jadx.api.plugins.input.data.annotations.EncodedType; 7 | import jadx.api.plugins.input.data.annotations.EncodedValue; 8 | import jadx.api.plugins.input.data.annotations.IAnnotation; 9 | import jadx.api.plugins.input.data.attributes.JadxAttrType; 10 | import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; 11 | import jadx.core.dex.instructions.ConstStringNode; 12 | import jadx.core.dex.nodes.InsnNode; 13 | import jadx.core.dex.nodes.MethodNode; 14 | import jadx.core.utils.exceptions.DecodeException; 15 | import java.util.ArrayList; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Set; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import ru.blackfan.bfscan.config.ConfigLoader; 23 | import ru.blackfan.bfscan.helpers.Helpers; 24 | import ru.blackfan.bfscan.helpers.KeyValuePair; 25 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 26 | 27 | public class Class { 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(Class.class); 30 | public static final List EXCLUDED_PACKAGES = ConfigLoader.getExcludedPackages(); 31 | 32 | public static Set process(String fileName, JavaClass cls) { 33 | Set keyValuePairs = new HashSet<>(); 34 | logger.debug("Process class " + fileName); 35 | 36 | for (String pkg : EXCLUDED_PACKAGES) { 37 | if (fileName.startsWith(pkg)) { 38 | return keyValuePairs; 39 | } 40 | } 41 | for (JavaField field : cls.getFields()) { 42 | Set stringConstants = new HashSet<>(); 43 | EncodedValue constVal = field.getFieldNode().get(JadxAttrType.CONSTANT_VALUE); 44 | if (constVal != null) { 45 | parseStrings(constVal, stringConstants, cls.getClassNode().getAlias()); 46 | for (String str : stringConstants) { 47 | logger.debug(" Class field constant: " + str); 48 | keyValuePairs.add(new KeyValuePair(field.getName(), str)); 49 | } 50 | } 51 | } 52 | 53 | AnnotationsAttr classAList = cls.getClassNode().get(JadxAttrType.ANNOTATION_LIST); 54 | if (classAList != null && !classAList.isEmpty()) { 55 | Set stringConstants = new HashSet<>(); 56 | extractStringFromAnnotations(classAList, stringConstants, cls.getClassNode().getAlias()); 57 | for (String str : stringConstants) { 58 | logger.debug(" Class annotation constant: " + str); 59 | keyValuePairs.add(new KeyValuePair("class_annotation", str)); 60 | } 61 | } 62 | 63 | for (JavaMethod mth : cls.getMethods()) { 64 | logger.debug(" Method: " + mth.getName()); 65 | if (!mth.getName().equals("toString")) { 66 | Set stringConstants = getStringConstants(mth); 67 | for (String str : stringConstants) { 68 | logger.debug(" String constant: " + str); 69 | keyValuePairs.add(new KeyValuePair(mth.getName(), str)); 70 | } 71 | } 72 | } 73 | return keyValuePairs; 74 | } 75 | 76 | private static Set getStringConstants(JavaMethod method) { 77 | Set stringConstants = new HashSet<>(); 78 | try { 79 | MethodNode methodNode = method.getMethodNode(); 80 | if (methodNode != null) { 81 | methodNode.load(); 82 | if (methodNode.getInstructions() == null && methodNode.getInsnsCount() != 0) { 83 | methodNode.reload(); 84 | } 85 | InsnNode[] instructions = methodNode.getInstructions(); 86 | if (instructions != null) { 87 | for (InsnNode insn : instructions) { 88 | if (insn instanceof ConstStringNode constStrInsn) { 89 | addConstant(constStrInsn.getString(), stringConstants, methodNode.getAlias()); 90 | } 91 | } 92 | } else { 93 | logger.debug("No instructions found for method: {}", method.getName()); 94 | } 95 | 96 | AnnotationsAttr aList = methodNode.get(JadxAttrType.ANNOTATION_LIST); 97 | if (aList != null && !aList.isEmpty()) { 98 | extractStringFromAnnotations(aList, stringConstants, methodNode.getAlias()); 99 | } 100 | } 101 | return stringConstants; 102 | } catch (DecodeException ex) { 103 | logger.error("Method DecodeException", ex); 104 | } 105 | return stringConstants; 106 | } 107 | 108 | private static void extractStringFromAnnotations(AnnotationsAttr annotations, Set stringConstants, String methodName) { 109 | for (IAnnotation annotation : annotations.getAll()) { 110 | if (!annotation.getAnnotationClass().startsWith(Constants.KOTLIN_METADATA)) { 111 | for (Map.Entry entry : annotation.getValues().entrySet()) { 112 | parseStrings(entry.getValue(), stringConstants, methodName); 113 | } 114 | } 115 | } 116 | } 117 | 118 | private static void parseStrings(EncodedValue value, Set stringConstants, String methodName) { 119 | if (value != null) { 120 | if (value.getType() == EncodedType.ENCODED_STRING) { 121 | addConstant(Helpers.stringWrapper(value), stringConstants, methodName); 122 | } else if (value.getType() == EncodedType.ENCODED_ARRAY) { 123 | for (Object obj : (ArrayList) value.getValue()) { 124 | if (((EncodedValue) obj).getType() == EncodedType.ENCODED_STRING) { 125 | addConstant(Helpers.stringWrapper((EncodedValue) obj), stringConstants, methodName); 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | private static void addConstant(String s, Set stringConstants, String methodName) { 133 | if (!Helpers.isJVMSig(s) && !s.equals(methodName)) { 134 | stringConstants.add(s); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/FeignProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.ILocalVar; 5 | import jadx.core.dex.instructions.args.ArgType; 6 | import jadx.core.dex.nodes.RootNode; 7 | import java.net.MalformedURLException; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.net.URL; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import ru.blackfan.bfscan.helpers.Helpers; 15 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 16 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 17 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 18 | import ru.blackfan.bfscan.parsing.httprequests.requestbody.RawRequestBody; 19 | 20 | public class FeignProcessor implements AnnotationProcessor { 21 | 22 | @Override 23 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 24 | ParameterInfo paramInfo, 25 | String annotationClass, 26 | Map annotationValues, 27 | List localVars, 28 | int methodArg, 29 | ArgType var, 30 | RootNode rn) { 31 | switch (annotationClass) { 32 | case Constants.Feign.QUERYMAP, Constants.Feign.PARAM -> { 33 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 34 | } 35 | case Constants.Feign.HEADERMAP -> { 36 | AnnotationUtils.processHeader(request, "HeaderName", "HeaderValue"); 37 | return ArgProcessingState.PARAMETER_CREATED; 38 | } 39 | default -> { 40 | return ArgProcessingState.NOT_PROCESSED; 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | public boolean processMethodAnnotations(MultiHTTPRequest request, 47 | String annotationClass, 48 | Map annotationValues, 49 | RootNode rn) { 50 | switch (annotationClass) { 51 | case Constants.Feign.BODY -> { 52 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 53 | if (value != null) { 54 | String body = Helpers.stringWrapper(value); 55 | if (body.startsWith("<")) { 56 | request.setEncType("xml"); 57 | } else if (body.toLowerCase().startsWith("%7b")) { 58 | body = body.replaceAll("(?i)%7B", "{").replaceAll("(?i)%7D", "}"); 59 | request.setEncType("json"); 60 | } 61 | request.setRequestBody(new RawRequestBody(body)); 62 | } 63 | return true; 64 | } 65 | case Constants.Feign.REQUESTLINE -> { 66 | request.addAdditionalInformation("Feign Method"); 67 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 68 | if (value != null) { 69 | String requestLine = Helpers.stringWrapper(value); 70 | String[] parts = requestLine.split(" ", 2); 71 | String method = parts[0]; 72 | request.setMethod(method); 73 | String path = parts.length > 1 ? parts[1] : ""; 74 | if (!path.isEmpty()) { 75 | request.setPath(path, true); 76 | } 77 | } 78 | return true; 79 | } 80 | case Constants.Feign.HEADERS -> { 81 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 82 | if (value != null) { 83 | AnnotationUtils.processHeadersFromList(request, (ArrayList) value.getValue()); 84 | } 85 | return true; 86 | } 87 | default -> { 88 | return false; 89 | } 90 | } 91 | } 92 | 93 | @Override 94 | public boolean processClassAnnotations(MultiHTTPRequest request, 95 | String annotationClass, 96 | Map annotationValues, 97 | String globalBasePath, 98 | String className, 99 | RootNode rn) { 100 | switch (annotationClass) { 101 | case Constants.Feign.CLIENT -> { 102 | request.addAdditionalInformation("Feign Client"); 103 | EncodedValue value; 104 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("value", "name"))) != null) { 105 | request.addAdditionalInformation("Client Name: " + Helpers.stringWrapper(value)); 106 | } 107 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("url"))) != null) { 108 | String url = Helpers.stringWrapper(value); 109 | if (url.matches("https?://.*")) { 110 | try { 111 | URI absUrl = new URL(url).toURI(); 112 | if (absUrl.getPort() != -1) { 113 | request.setHost(absUrl.getHost() + ":" + absUrl.getPort()); 114 | } else { 115 | request.setHost(absUrl.getHost()); 116 | } 117 | String path = absUrl.getPath(); 118 | if ((path != null) && !path.isEmpty() && !path.equals("/")) { 119 | String classPath = path; 120 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 121 | request.setBasePaths(List.of(fullPath)); 122 | } 123 | } catch (MalformedURLException | URISyntaxException e) { 124 | } 125 | } else { 126 | request.addAdditionalInformation("Unrecognized FeignClient url attribute: " + Helpers.stringWrapper(value)); 127 | } 128 | } 129 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("path"))) != null) { 130 | String classPath = Helpers.stringWrapper(value); 131 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 132 | request.setBasePaths(List.of(fullPath)); 133 | } 134 | return false; 135 | } 136 | default -> { 137 | return false; 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/FieldAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedType; 4 | import jadx.api.plugins.input.data.annotations.EncodedValue; 5 | import jadx.api.plugins.input.data.annotations.IAnnotation; 6 | import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; 7 | import jadx.core.dex.nodes.RootNode; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Map; 11 | import ru.blackfan.bfscan.config.ProcessorConfig; 12 | import ru.blackfan.bfscan.helpers.Helpers; 13 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 14 | 15 | public class FieldAnnotationProcessor { 16 | 17 | private final RootNode rootNode; 18 | private String fieldName; 19 | private String defaultValue; 20 | 21 | public FieldAnnotationProcessor(RootNode rootNode) { 22 | this.rootNode = rootNode; 23 | } 24 | 25 | public void process(AnnotationsAttr aList, String initialFieldName, String initialDefaultValue) { 26 | this.fieldName = initialFieldName; 27 | this.defaultValue = initialDefaultValue; 28 | 29 | for (IAnnotation a : aList.getAll()) { 30 | processAnnotation(a); 31 | } 32 | } 33 | 34 | private void processAnnotation(IAnnotation annotation) { 35 | Map annotationValues = annotation.getValues(); 36 | String annotationClass = AnnotationUtils.getAnnotationClass(annotation, rootNode); 37 | if(Constants.Ignore.IGNORE_ANNOTATIONS.contains(annotationClass)) { 38 | return; 39 | } 40 | switch (annotationClass) { 41 | case Constants.Serialization.GSON_SERIALIZED_NAME, Constants.Serialization.KOTLINX_SERIALNAME, 42 | Constants.JaxRs.PARAM_FORM, Constants.Jakarta.PARAM_FORM, 43 | Constants.JaxRs.PARAM_QUERY, Constants.Jakarta.PARAM_QUERY, 44 | Constants.JaxRs.PARAM_COOKIE, Constants.Jakarta.PARAM_COOKIE, 45 | Constants.JaxRs.PARAM_HEADER, Constants.Jakarta.PARAM_HEADER, 46 | Constants.JaxRs.PARAM_MATRIX, Constants.Jakarta.PARAM_MATRIX, 47 | Constants.Serialization.JAX_JSONBPROPERTY, 48 | Constants.Serialization.JAKARTA_JSONBPROPERTY, 49 | Constants.Serialization.APACHE_JOHNZON_PROPERTY, 50 | Constants.Spring.BIND_PARAM, Constants.Serialization.CODEHAUS_JSON_PROPERTY, 51 | Constants.Serialization.XSTREAM_ALIAS -> { 52 | processNameValue(annotationValues); 53 | } 54 | case Constants.Serialization.SQUAREUP_MOSHI_JSON, Constants.Serialization.FASTJSON_JSONFIELD, 55 | Constants.Serialization.FASTJSON2_JSONFIELD, Constants.Serialization.JAX_COLUMN-> { 56 | processNameName(annotationValues); 57 | } 58 | case Constants.Serialization.JAKARTA_XML_ELEMENT, Constants.Serialization.JAX_XML_ELEMENT -> { 59 | processXmlElement(annotationValues); 60 | } 61 | case Constants.Serialization.JACKSON_JSON_PROPERTY -> { 62 | processJsonProperty(annotationValues); 63 | } 64 | case Constants.Swagger.API_PARAM, Constants.Swagger.API_MODEL_PROPERTY -> { 65 | processSwaggerAnnotation(annotationValues); 66 | } 67 | case Constants.OpenApi.SCHEMA -> { 68 | processOpenApiAnnotation(annotationValues); 69 | } 70 | default -> { 71 | if(ProcessorConfig.getInstance().isMinifiedAnnotationsSupport()) { 72 | minifiedAnnotation(annotationValues); 73 | } 74 | } 75 | } 76 | } 77 | 78 | private void minifiedAnnotation(Map values) { 79 | EncodedValue value = AnnotationUtils.getValue(values, List.of("value", "name", "a")); 80 | if (value != null && value.getType() == EncodedType.ENCODED_STRING) { 81 | String potentialFieldName = Helpers.stringWrapper(value); 82 | if (potentialFieldName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) { 83 | fieldName = potentialFieldName; 84 | } 85 | } 86 | } 87 | 88 | private void processNameValue(Map values) { 89 | EncodedValue value = AnnotationUtils.getValue(values, List.of("value", "a")); 90 | if (value != null) { 91 | fieldName = Helpers.stringWrapper(value); 92 | } 93 | } 94 | 95 | private void processNameName(Map values) { 96 | EncodedValue value = AnnotationUtils.getValue(values, List.of("name")); 97 | if (value != null) { 98 | fieldName = Helpers.stringWrapper(value); 99 | } 100 | } 101 | 102 | private void processXmlElement(Map values) { 103 | EncodedValue value = AnnotationUtils.getValue(values, List.of("name")); 104 | if (value != null) { 105 | fieldName = Helpers.stringWrapper(value); 106 | } 107 | value = AnnotationUtils.getValue(values, List.of("defaultValue")); 108 | if (value != null) { 109 | defaultValue = Helpers.stringWrapper(value); 110 | } 111 | } 112 | 113 | private void processJsonProperty(Map values) { 114 | EncodedValue value = AnnotationUtils.getValue(values, List.of("value")); 115 | if (value != null) { 116 | fieldName = Helpers.stringWrapper(value); 117 | } 118 | value = AnnotationUtils.getValue(values, List.of("defaultValue")); 119 | if (value != null) { 120 | defaultValue = Helpers.stringWrapper(value); 121 | } 122 | } 123 | 124 | private void processSwaggerAnnotation(Map values) { 125 | EncodedValue value = AnnotationUtils.getValue(values, List.of("name")); 126 | if (value != null) { 127 | fieldName = Helpers.stringWrapper(value); 128 | } 129 | value = AnnotationUtils.getValue(values, List.of("defaultValue", "example", "allowableValues")); 130 | if (value != null) { 131 | defaultValue = Helpers.stringWrapper(value); 132 | } 133 | } 134 | 135 | private void processOpenApiAnnotation(Map values) { 136 | EncodedValue value = AnnotationUtils.getValue(values, List.of("name")); 137 | if (value != null) { 138 | fieldName = Helpers.stringWrapper(value); 139 | } 140 | value = AnnotationUtils.getValue(values, List.of("defaultValue", "example")); 141 | if (value != null) { 142 | defaultValue = Helpers.stringWrapper(value); 143 | } else { 144 | value = AnnotationUtils.getValue(values, List.of("allowableValues", "examples")); 145 | if (value != null) { 146 | ArrayList examples = (ArrayList) value.getValue(); 147 | if (!examples.isEmpty()) { 148 | defaultValue = Helpers.stringWrapper(examples.get(0)); 149 | } 150 | } 151 | } 152 | } 153 | 154 | public String getFieldName() { 155 | return fieldName; 156 | } 157 | 158 | public String getDefaultValue() { 159 | return defaultValue; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/RetrofitProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.ILocalVar; 5 | import jadx.core.dex.instructions.args.ArgType; 6 | import jadx.core.dex.nodes.RootNode; 7 | import java.net.MalformedURLException; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.net.URL; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import ru.blackfan.bfscan.helpers.Helpers; 15 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 16 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 17 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 18 | 19 | public class RetrofitProcessor implements AnnotationProcessor { 20 | 21 | @Override 22 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 23 | ParameterInfo paramInfo, 24 | String annotationClass, 25 | Map annotationValues, 26 | List localVars, 27 | int methodArg, 28 | ArgType argType, 29 | RootNode rootNode) throws Exception { 30 | switch (annotationClass) { 31 | case Constants.Retrofit.PARAM_QUERY, Constants.Ktorfit.PARAM_QUERY -> { 32 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 33 | AnnotationUtils.processQueryParameter(request, paramName, paramInfo.getDefaultValue(), argType, rootNode); 34 | return ArgProcessingState.PARAMETER_CREATED; 35 | } 36 | case Constants.Retrofit.PARAM_HEADER, Constants.Ktorfit.PARAM_HEADER -> { 37 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 38 | AnnotationUtils.processHeader(request, paramName, paramInfo.getDefaultValue()); 39 | return ArgProcessingState.PARAMETER_CREATED; 40 | } 41 | case Constants.Retrofit.PARAM_PATH, Constants.Ktorfit.PARAM_PATH -> { 42 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 43 | if (paramName != null) { 44 | request.addPathParameter(paramName); 45 | } 46 | return ArgProcessingState.PARAMETER_CREATED; 47 | } 48 | case Constants.Retrofit.PARAM_FIELD, Constants.Ktorfit.PARAM_FIELD -> { 49 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 50 | AnnotationUtils.processBodyParameter(request, paramName, paramInfo.getDefaultValue(), argType, rootNode); 51 | return ArgProcessingState.PARAMETER_CREATED; 52 | } 53 | case Constants.Retrofit.PARAM_PART, Constants.Ktorfit.PARAM_PART -> { 54 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value")), paramInfo, localVars, methodArg); 55 | AnnotationUtils.processPart(request, paramName); 56 | return ArgProcessingState.PARAMETER_CREATED; 57 | } 58 | case Constants.Retrofit.PARAM_BODY, Constants.Ktorfit.PARAM_BODY -> { 59 | String paramName = AnnotationUtils.getParamName(null, paramInfo, localVars, methodArg); 60 | AnnotationUtils.processArbitraryBodyParameter(request, paramName, argType, rootNode); 61 | return ArgProcessingState.PARAMETER_CREATED; 62 | } 63 | case Constants.Retrofit.PARAM_QUERYMAP, Constants.Ktorfit.PARAM_QUERYMAP -> { 64 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 65 | } 66 | case Constants.Retrofit.PARAM_HEADERMAP, Constants.Ktorfit.PARAM_HEADERMAP -> { 67 | AnnotationUtils.processHeader(request, "HeaderName", "HeaderValue"); 68 | return ArgProcessingState.PARAMETER_CREATED; 69 | } 70 | default -> { 71 | return ArgProcessingState.NOT_PROCESSED; 72 | } 73 | } 74 | } 75 | 76 | @Override 77 | public boolean processMethodAnnotations(MultiHTTPRequest request, 78 | String annotationClass, 79 | Map annotationValues, 80 | RootNode rn) { 81 | switch (annotationClass) { 82 | 83 | case Constants.Retrofit.HEAD, Constants.Retrofit.GET, Constants.Retrofit.POST, Constants.Retrofit.PUT, Constants.Retrofit.DELETE, Constants.Retrofit.PATCH, Constants.Retrofit.OPTIONS, Constants.Ktorfit.HEAD, Constants.Ktorfit.GET, Constants.Ktorfit.POST, Constants.Ktorfit.PUT, Constants.Ktorfit.DELETE, Constants.Ktorfit.PATCH, Constants.Ktorfit.OPTIONS -> { 84 | request.setMethod(Helpers.classSigToRawShortName(annotationClass)); 85 | request.addAdditionalInformation("Retrofit/Ktorfit Method"); 86 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("path", "value", "a")); 87 | if (value != null) { 88 | if (Helpers.stringWrapper(value).matches("https?://.*")) { 89 | try { 90 | URI absUrl = new URL(Helpers.stringWrapper(value)).toURI(); 91 | request.setPath(absUrl.getPath(), true); 92 | request.setHost(absUrl.getHost()); 93 | } catch (MalformedURLException | URISyntaxException e) { 94 | } 95 | } else { 96 | request.setPath(Helpers.stringWrapper(value), true); 97 | } 98 | } 99 | return true; 100 | } 101 | case Constants.Retrofit.HTTP, Constants.Ktorfit.HTTP -> { 102 | request.addAdditionalInformation("Retrofit/Ktorfit Method"); 103 | if (annotationValues.get("method") != null) { 104 | request.setMethod(Helpers.stringWrapper(annotationValues.get("method"))); 105 | } 106 | if (annotationValues.get("path") != null) { 107 | request.setPath(Helpers.stringWrapper(annotationValues.get("path")), true); 108 | } 109 | return true; 110 | } 111 | case Constants.Retrofit.MULTIPART, Constants.Ktorfit.MULTIPART -> { 112 | request.setEncType("multipart"); 113 | return true; 114 | } 115 | case Constants.Retrofit.FORM, Constants.Ktorfit.FORM -> { 116 | request.setEncType("form"); 117 | return true; 118 | } 119 | case Constants.Retrofit.HEADERS, Constants.Ktorfit.HEADERS -> { 120 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 121 | if (value != null) { 122 | AnnotationUtils.processHeadersFromList(request, (ArrayList) value.getValue()); 123 | } 124 | return true; 125 | } 126 | default -> { 127 | return false; 128 | } 129 | } 130 | } 131 | 132 | @Override 133 | public boolean processClassAnnotations(MultiHTTPRequest request, 134 | String annotationClass, 135 | Map annotationValues, 136 | String globalBasePath, 137 | String className, 138 | RootNode rn) { 139 | return false; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/MultiHTTPRequest.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import ru.blackfan.bfscan.parsing.httprequests.requestbody.ObjectRequestBody; 7 | import ru.blackfan.bfscan.parsing.httprequests.requestbody.RequestBody; 8 | 9 | public class MultiHTTPRequest { 10 | 11 | private List requests; 12 | private String className; 13 | private String methodName; 14 | private static final String CONTENT_TYPE_HEADER = "content-type"; 15 | public static final Map CONTENT_TYPE_TO_ENC_TYPE = Map.of( 16 | "application/x-www-form-urlencoded", "form", 17 | "multipart/form-data", "multipart", 18 | "application/json", "json", 19 | "application/xml", "xml", 20 | "text/xml", "xml" 21 | ); 22 | 23 | public MultiHTTPRequest(String apiHost, String apiBasePath, String className, String methodName) { 24 | this.className = className; 25 | this.methodName = methodName; 26 | this.requests = new ArrayList<>(); 27 | HTTPRequest req = new HTTPRequest(); 28 | req.basePath = apiBasePath; 29 | req.host = apiHost; 30 | this.requests.add(req); 31 | } 32 | 33 | public MultiHTTPRequest(MultiHTTPRequest multiRequest, String className, String methodName) { 34 | List newRequests = new ArrayList<>(); 35 | for (HTTPRequest request : multiRequest.requests) { 36 | newRequests.add(new HTTPRequest(request)); 37 | } 38 | this.requests = newRequests; 39 | this.className = className; 40 | this.methodName = methodName; 41 | } 42 | 43 | public String getClassName() { 44 | return className; 45 | } 46 | 47 | public String getMethodName() { 48 | return methodName; 49 | } 50 | 51 | public void addPathParameter(String pathParameter) { 52 | for (HTTPRequest request : requests) { 53 | if (request.path.contains("{" + pathParameter + "}") && !request.pathParams.contains(pathParameter)) { 54 | request.pathParams.add(pathParameter); 55 | } 56 | } 57 | } 58 | 59 | public void addAdditionalInformation(String additionalInformation) { 60 | for (HTTPRequest request : requests) { 61 | request.additionalInformation.add(additionalInformation); 62 | } 63 | } 64 | 65 | public void putQueryParameter(String key, String value) { 66 | for (HTTPRequest request : requests) { 67 | request.queryParameters.put(key, value); 68 | } 69 | } 70 | 71 | public void putCookieParameter(String key, String value) { 72 | for (HTTPRequest request : requests) { 73 | request.cookieParameters.put(key, value); 74 | } 75 | } 76 | 77 | public void putHeader(String key, String value) { 78 | for (HTTPRequest request : requests) { 79 | if (key.toLowerCase().equals(CONTENT_TYPE_HEADER)) { 80 | String[] valueParts = value.split(";", 2); 81 | String[] contentTypes = valueParts[0].split(","); 82 | String selectedContentType = null; 83 | 84 | for (String contentType : contentTypes) { 85 | contentType = contentType.trim().toLowerCase(); 86 | if (CONTENT_TYPE_TO_ENC_TYPE.containsKey(contentType)) { 87 | selectedContentType = contentType; 88 | break; 89 | } 90 | } 91 | 92 | if (selectedContentType != null) { 93 | setEncType(CONTENT_TYPE_TO_ENC_TYPE.get(selectedContentType)); 94 | } 95 | } 96 | request.headers.put(key, value); 97 | } 98 | } 99 | 100 | public void putBodyParameter(String key, Object value) throws Exception { 101 | for (HTTPRequest request : requests) { 102 | if (request.requestBody == null) { 103 | request.requestBody = new ObjectRequestBody(); 104 | } 105 | if (request.requestBody instanceof ObjectRequestBody) { 106 | ((ObjectRequestBody) request.requestBody).putBodyParameter(key, value); 107 | } else { 108 | request.requestBody = new ObjectRequestBody(); 109 | ((ObjectRequestBody) request.requestBody).putBodyParameter(key, value); 110 | } 111 | if (request.method.equals("GET")) { 112 | request.method = "POST"; 113 | } 114 | } 115 | } 116 | 117 | public void putBodyParameters(Map parameters) throws Exception { 118 | if (!parameters.isEmpty()) { 119 | for (HTTPRequest request : requests) { 120 | if (request.requestBody == null) { 121 | request.requestBody = new ObjectRequestBody(); 122 | } 123 | if (request.requestBody instanceof ObjectRequestBody) { 124 | ((ObjectRequestBody) request.requestBody).putBodyParameters(parameters); 125 | } else { 126 | request.requestBody = new ObjectRequestBody(); 127 | ((ObjectRequestBody) request.requestBody).putBodyParameters(parameters); 128 | } 129 | if (request.method.equals("GET")) { 130 | request.method = "POST"; 131 | } 132 | } 133 | } 134 | } 135 | 136 | public void setRequestBody(RequestBody requestBody) { 137 | for (HTTPRequest request : requests) { 138 | if (requestBody != null && request.method.equals("GET")) { 139 | request.method = "POST"; 140 | } 141 | if (request.requestBody == null) { 142 | request.requestBody = requestBody; 143 | } 144 | } 145 | } 146 | 147 | public void setEncType(String encType) { 148 | for (HTTPRequest request : requests) { 149 | request.encType = encType; 150 | } 151 | } 152 | 153 | public void setHost(String host) { 154 | for (HTTPRequest request : requests) { 155 | request.host = host; 156 | } 157 | } 158 | 159 | public void setPath(String path, boolean query) { 160 | if (path.contains("?") && query) { 161 | String[] urlParts = path.split("\\?", 2); 162 | path = urlParts[0]; 163 | String[] params = urlParts[1].split("&"); 164 | for (String param : params) { 165 | String[] keyValue = param.split("=", 2); 166 | String qKey = keyValue[0]; 167 | String qValue = keyValue.length > 1 ? keyValue[1] : ""; 168 | if (!qKey.isEmpty() && !qValue.isEmpty()) { 169 | putQueryParameter(qKey, qValue); 170 | } 171 | } 172 | } 173 | for (HTTPRequest request : requests) { 174 | request.path = path; 175 | } 176 | } 177 | 178 | public void setPaths(List paths) { 179 | duplicateRequests(paths, "path"); 180 | } 181 | 182 | public void setMethods(List methods) { 183 | duplicateRequests(methods, "method"); 184 | } 185 | 186 | public void setBasePaths(List basePaths) { 187 | duplicateRequests(basePaths, "basePath"); 188 | } 189 | 190 | public void setMethod(String method) { 191 | for (HTTPRequest request : requests) { 192 | request.method = method; 193 | } 194 | } 195 | 196 | private void duplicateRequests(List values, String type) { 197 | if (requests.isEmpty() || values.isEmpty()) { 198 | return; 199 | } 200 | 201 | List newRequests = new ArrayList<>(); 202 | 203 | for (HTTPRequest request : new ArrayList<>(requests)) { 204 | for (String value : values) { 205 | HTTPRequest copy = new HTTPRequest(request); 206 | switch (type) { 207 | case "path" -> 208 | copy.path = value; 209 | case "method" -> 210 | copy.method = value; 211 | case "basePath" -> 212 | copy.basePath = value; 213 | } 214 | newRequests.add(copy); 215 | } 216 | } 217 | requests.clear(); 218 | requests.addAll(newRequests); 219 | } 220 | 221 | public List getRequests() { 222 | return this.requests; 223 | } 224 | 225 | public List getPathParameters() { 226 | if (!requests.isEmpty()) { 227 | return requests.get(0).pathParams; 228 | } 229 | return new ArrayList<>(); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/SwaggerProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.annotations.JadxAnnotation; 5 | import jadx.api.plugins.input.data.ILocalVar; 6 | import jadx.core.dex.instructions.args.ArgType; 7 | import jadx.core.dex.nodes.ClassNode; 8 | import jadx.core.dex.nodes.RootNode; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Map; 12 | import ru.blackfan.bfscan.helpers.Helpers; 13 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 14 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 15 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 16 | 17 | public class SwaggerProcessor implements AnnotationProcessor { 18 | 19 | @Override 20 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 21 | ParameterInfo paramInfo, 22 | String annotationClass, 23 | Map annotationValues, 24 | List localVars, 25 | int methodArg, 26 | ArgType var, 27 | RootNode rn) throws Exception { 28 | switch (annotationClass) { 29 | case Constants.Swagger.API_PARAM -> { 30 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("name")), paramInfo, localVars, methodArg); 31 | if (paramName != null) { 32 | String paramValue = paramName; 33 | EncodedValue value; 34 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("defaultValue", "example", "allowableValues"))) != null) { 35 | paramValue = Helpers.stringWrapper(value); 36 | } 37 | request.putQueryParameter(paramName, paramValue); 38 | return ArgProcessingState.PARAMETER_CREATED; 39 | } 40 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 41 | } 42 | default -> { 43 | return ArgProcessingState.NOT_PROCESSED; 44 | } 45 | } 46 | } 47 | 48 | @Override 49 | public boolean processMethodAnnotations(MultiHTTPRequest request, 50 | String annotationClass, 51 | Map annotationValues, 52 | RootNode rn) throws Exception { 53 | switch (annotationClass) { 54 | case Constants.Swagger.API -> { 55 | EncodedValue value; 56 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("value", "basePath"))) != null) { 57 | request.setBasePaths(List.of(Helpers.stringWrapper(value))); 58 | } 59 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("description"))) != null) { 60 | request.addAdditionalInformation("API Description: " + Helpers.stringWrapper(value)); 61 | } 62 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("consumes"))) != null) { 63 | request.putHeader("Content-Type", Helpers.stringWrapper(value)); 64 | } 65 | return true; 66 | } 67 | case Constants.Swagger.API_OPERATION -> { 68 | EncodedValue value; 69 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("value"))) != null) { 70 | request.addAdditionalInformation("Operation: " + Helpers.stringWrapper(value)); 71 | } 72 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("notes"))) != null) { 73 | request.addAdditionalInformation("Notes: " + Helpers.stringWrapper(value)); 74 | } 75 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("httpMethod"))) != null) { 76 | request.setMethod(Helpers.stringWrapper(value)); 77 | } 78 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("consumes"))) != null) { 79 | request.putHeader("Content-Type", Helpers.stringWrapper(value)); 80 | } 81 | request.addAdditionalInformation("Swagger ApiOperation"); 82 | return true; 83 | } 84 | case Constants.Swagger.API_IMPLICIT_PARAM -> { 85 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("name")); 86 | if (value != null) { 87 | String paramName = Helpers.stringWrapper(value); 88 | String paramValue = paramName; 89 | String paramType = "query"; 90 | String dataType = null; 91 | 92 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("defaultValue", "example"))) != null) { 93 | paramValue = Helpers.stringWrapper(value); 94 | } 95 | 96 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("paramType"))) != null) { 97 | paramType = Helpers.stringWrapper(value); 98 | } 99 | 100 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("dataType"))) != null) { 101 | dataType = Helpers.stringWrapper(value); 102 | } 103 | 104 | switch (paramType) { 105 | case "query" -> 106 | request.putQueryParameter(paramName, paramValue); 107 | case "header" -> 108 | request.putHeader(paramName, paramValue); 109 | case "path" -> 110 | request.addPathParameter(paramName); 111 | case "form" -> 112 | request.putBodyParameter(paramName, paramValue); 113 | case "body" -> { 114 | ClassNode classNode = Helpers.loadClass(rn, dataType); 115 | if(classNode != null) { 116 | AnnotationUtils.processArbitraryBodyParameter(request, paramName, classNode.getType(), rn); 117 | } 118 | } 119 | } 120 | } 121 | return true; 122 | } 123 | case Constants.Swagger.API_IMPLICIT_PARAMS -> { 124 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 125 | if (value != null) { 126 | ArrayList params = (ArrayList) value.getValue(); 127 | for (EncodedValue param : params) { 128 | if (param.getValue() != null) { 129 | Map paramValues = ((JadxAnnotation) param.getValue()).getValues(); 130 | processMethodAnnotations(request, Constants.Swagger.API_IMPLICIT_PARAM, paramValues, rn); 131 | } 132 | } 133 | } 134 | return true; 135 | } 136 | default -> { 137 | return false; 138 | } 139 | } 140 | } 141 | 142 | @Override 143 | public boolean processClassAnnotations(MultiHTTPRequest request, 144 | String annotationClass, 145 | Map annotationValues, 146 | String globalBasePath, 147 | String className, 148 | RootNode rn 149 | ) { 150 | switch (annotationClass) { 151 | case Constants.Swagger.API -> { 152 | EncodedValue value; 153 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("value", "basePath"))) != null) { 154 | request.setBasePaths(List.of(Helpers.stringWrapper(value))); 155 | } 156 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("description"))) != null) { 157 | request.addAdditionalInformation("API Description: " + Helpers.stringWrapper(value)); 158 | } 159 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("consumes"))) != null) { 160 | request.putHeader("Content-Type", Helpers.stringWrapper(value)); 161 | } 162 | return false; 163 | } 164 | case Constants.Swagger.API_MODEL -> { 165 | EncodedValue value; 166 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("value"))) != null) { 167 | request.addAdditionalInformation("Model: " + Helpers.stringWrapper(value)); 168 | } 169 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("description"))) != null) { 170 | request.addAdditionalInformation("Model Description: " + Helpers.stringWrapper(value)); 171 | } 172 | return false; 173 | } 174 | default -> { 175 | return false; 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/JaxJakartaProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.ILocalVar; 5 | import jadx.core.dex.instructions.args.ArgType; 6 | import jadx.core.dex.nodes.RootNode; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import ru.blackfan.bfscan.helpers.Helpers; 11 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 12 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 13 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 14 | 15 | public class JaxJakartaProcessor implements AnnotationProcessor { 16 | 17 | @Override 18 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 19 | ParameterInfo paramInfo, 20 | String annotationClass, 21 | Map annotationValues, 22 | List localVars, 23 | int methodArg, 24 | ArgType argType, 25 | RootNode rootNode) throws Exception { 26 | switch (annotationClass) { 27 | case Constants.JaxRs.PARAM_QUERY, Constants.Jakarta.PARAM_QUERY -> { 28 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 29 | AnnotationUtils.processQueryParameter(request, paramName, paramInfo.getDefaultValue(), argType, rootNode); 30 | return ArgProcessingState.PARAMETER_CREATED; 31 | } 32 | case Constants.JaxRs.PARAM_PATH, Constants.Jakarta.PARAM_PATH -> { 33 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 34 | if (paramName != null) { 35 | request.addPathParameter(paramName); 36 | } 37 | return ArgProcessingState.PARAMETER_CREATED; 38 | } 39 | case Constants.JaxRs.PARAM_HEADER, Constants.Jakarta.PARAM_HEADER -> { 40 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 41 | AnnotationUtils.processHeader(request, paramName, paramInfo.getDefaultValue()); 42 | return ArgProcessingState.PARAMETER_CREATED; 43 | } 44 | case Constants.JaxRs.PARAM_FORM, Constants.Jakarta.PARAM_FORM -> { 45 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 46 | AnnotationUtils.processBodyParameter(request, paramName, paramInfo.getDefaultValue(), argType, rootNode); 47 | return ArgProcessingState.PARAMETER_CREATED; 48 | } 49 | case Constants.JaxRs.PARAM_COOKIE, Constants.Jakarta.PARAM_COOKIE -> { 50 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "a")), paramInfo, localVars, methodArg); 51 | AnnotationUtils.processCookieParameter(request, paramName, paramInfo.getDefaultValue()); 52 | return ArgProcessingState.PARAMETER_CREATED; 53 | } 54 | case Constants.JaxRs.PARAM_MATRIX, Constants.Jakarta.PARAM_MATRIX -> { 55 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 56 | } 57 | case Constants.JaxRs.PARAM_BEAN, Constants.Jakarta.PARAM_BEAN -> { 58 | AnnotationUtils.processArbitraryBodyParameter(request, null, argType, rootNode); 59 | return ArgProcessingState.PARAMETER_CREATED; 60 | } 61 | case Constants.JaxRs.DEFAULT_VALUE, Constants.Jakarta.DEFAULT_VALUE -> { 62 | EncodedValue defaultValue = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 63 | if (defaultValue != null) { 64 | paramInfo.setDefaultValue(Helpers.stringWrapper(defaultValue)); 65 | } 66 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 67 | } 68 | default -> { 69 | return ArgProcessingState.NOT_PROCESSED; 70 | } 71 | } 72 | } 73 | 74 | @Override 75 | public boolean processMethodAnnotations(MultiHTTPRequest request, 76 | String annotationClass, 77 | Map annotationValues, 78 | RootNode rn) { 79 | switch (annotationClass) { 80 | case Constants.JaxRs.GET, Constants.Jakarta.GET, Constants.JaxRs.POST, Constants.Jakarta.POST, Constants.JaxRs.PUT, Constants.Jakarta.PUT, Constants.JaxRs.DELETE, Constants.Jakarta.DELETE, Constants.JaxRs.HEAD, Constants.Jakarta.HEAD, Constants.JaxRs.OPTIONS, Constants.Jakarta.OPTIONS -> { 81 | request.setMethod(Helpers.classSigToRawShortName(annotationClass)); 82 | request.addAdditionalInformation("JAX-RS/Jakarta Method"); 83 | return true; 84 | } 85 | case Constants.JaxRs.METHOD, Constants.Jakarta.METHOD -> { 86 | request.addAdditionalInformation("Jax/Jakarta Method"); 87 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 88 | if (value != null) { 89 | request.setMethod(Helpers.stringWrapper(value)); 90 | } 91 | request.addAdditionalInformation("JAX-RS/Jakarta Method"); 92 | return true; 93 | } 94 | case Constants.JaxRs.PATH, Constants.Jakarta.PATH -> { 95 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 96 | if (value != null) { 97 | request.setPath(Helpers.stringWrapper(value), false); 98 | } 99 | return true; 100 | } 101 | case Constants.JaxRs.PRODUCES, Constants.Jakarta.PRODUCES -> { 102 | return true; 103 | } 104 | case Constants.JaxRs.CONSUMES, Constants.Jakarta.CONSUMES -> { 105 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 106 | if (value != null) { 107 | ArrayList consumesList = (ArrayList) value.getValue(); 108 | if (!consumesList.isEmpty()) { 109 | AnnotationUtils.processContentTypeFromList(request, consumesList); 110 | } 111 | } 112 | return true; 113 | } 114 | default -> { 115 | return false; 116 | } 117 | } 118 | } 119 | 120 | @Override 121 | public boolean processClassAnnotations(MultiHTTPRequest request, 122 | String annotationClass, 123 | Map annotationValues, 124 | String globalBasePath, 125 | String className, 126 | RootNode rn) { 127 | switch (annotationClass) { 128 | case Constants.JaxRs.PATH, Constants.Jakarta.PATH -> { 129 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 130 | if (value != null) { 131 | String classPath = Helpers.stringWrapper(value); 132 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 133 | request.setBasePaths(List.of(fullPath)); 134 | } 135 | return false; 136 | } 137 | case Constants.JaxRs.WEBFILTER, Constants.Jakarta.WEBFILTER, Constants.JaxRs.WEBSERVLET, Constants.Jakarta.WEBSERVLET -> { 138 | request.addAdditionalInformation("Jax/Jakarta Webfilter/Webservlet"); 139 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "urlPatterns")); 140 | if (value != null) { 141 | ArrayList pathList = (ArrayList) value.getValue(); 142 | List paths = new ArrayList(); 143 | for (EncodedValue path : pathList) { 144 | String classPath = Helpers.stringWrapper(path); 145 | paths.add(classPath); 146 | } 147 | request.setPaths(paths); 148 | List httpMethods = AnnotationUtils.extractHttpMethodsFromServletClass(rn, className); 149 | if (!httpMethods.isEmpty()) { 150 | request.setMethods(httpMethods); 151 | } 152 | } 153 | return true; 154 | } 155 | case Constants.JaxRs.CONSUMES, Constants.Jakarta.CONSUMES -> { 156 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 157 | if (value != null) { 158 | ArrayList consumesList = (ArrayList) value.getValue(); 159 | if (!consumesList.isEmpty()) { 160 | AnnotationUtils.processContentTypeFromList(request, consumesList); 161 | } 162 | } 163 | return false; 164 | } 165 | default -> { 166 | return false; 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/helpers/Helpers.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.helpers; 2 | 3 | import io.swagger.v3.oas.models.PathItem; 4 | import jadx.api.plugins.input.data.annotations.EncodedType; 5 | import jadx.api.plugins.input.data.annotations.EncodedValue; 6 | import jadx.api.ResourceFile; 7 | import jadx.api.ResourceType; 8 | import jadx.core.dex.nodes.ClassNode; 9 | import jadx.core.dex.nodes.RootNode; 10 | import jadx.core.xmlgen.ResContainer; 11 | import java.io.ByteArrayInputStream; 12 | import java.io.File; 13 | import java.io.FileOutputStream; 14 | import java.io.InputStream; 15 | import java.io.IOException; 16 | import java.io.StringWriter; 17 | import java.net.URI; 18 | import java.net.URISyntaxException; 19 | import java.nio.charset.Charset; 20 | import java.nio.charset.StandardCharsets; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.regex.Pattern; 24 | import java.util.zip.ZipFile; 25 | import javax.xml.parsers.DocumentBuilder; 26 | import javax.xml.parsers.DocumentBuilderFactory; 27 | import javax.xml.parsers.ParserConfigurationException; 28 | import javax.xml.transform.dom.DOMSource; 29 | import javax.xml.transform.OutputKeys; 30 | import javax.xml.transform.stream.StreamResult; 31 | import javax.xml.transform.Transformer; 32 | import javax.xml.transform.TransformerException; 33 | import javax.xml.transform.TransformerFactory; 34 | import org.w3c.dom.Document; 35 | import org.w3c.dom.Element; 36 | 37 | public class Helpers { 38 | 39 | private static final Pattern VALID_URL_PATH_PATTERN = Pattern.compile("^[a-zA-Z0-9-._~!$&'()*+,;=:@/?#%{}]*$"); 40 | 41 | public static boolean isValidRequestHeader(String header) { 42 | String regex = "^[A-Z].*-.*$"; 43 | boolean matchesPattern = header.matches(regex); 44 | boolean containsAuthorization = header.toLowerCase().contains("authorization"); 45 | return matchesPattern || containsAuthorization; 46 | } 47 | 48 | public static boolean isValidURI(String input) { 49 | try { 50 | URI uri = new URI(input); 51 | return uri.getScheme() != null; 52 | } catch (URISyntaxException e) { 53 | return false; 54 | } 55 | } 56 | 57 | public static boolean isValidUrlPath(String path) { 58 | if (path == null) { 59 | return false; 60 | } 61 | path = path.replaceAll("^\\.+", ""); 62 | if (!path.startsWith("/")) { 63 | return false; 64 | } 65 | if (!VALID_URL_PATH_PATTERN.matcher(path).matches()) { 66 | return false; 67 | } 68 | try { 69 | URI uri = new URI("http", "example.com", path, null); 70 | return true; 71 | } catch (URISyntaxException e) { 72 | return false; 73 | } 74 | } 75 | 76 | public static ClassNode loadClass(RootNode root, String className) { 77 | ClassNode clsNode = root.resolveClass(className); 78 | if (clsNode != null) { 79 | clsNode.load(); 80 | clsNode.decompile(); 81 | return clsNode; 82 | } 83 | return null; 84 | } 85 | 86 | public static String getJvmAlias(RootNode root, String className) { 87 | ClassNode clsNode = loadClass(root, className); 88 | if (clsNode != null) { 89 | String alias = clsNode.getClassInfo().getAliasFullName(); 90 | if (alias != null) { 91 | return "L" + alias.replace(".", "/") + ";"; 92 | } 93 | } 94 | return null; 95 | } 96 | 97 | public static boolean isValidHttpMethod(String method) { 98 | for (PathItem.HttpMethod httpMethod : PathItem.HttpMethod.values()) { 99 | if (httpMethod.name().equalsIgnoreCase(method)) { 100 | return true; 101 | } 102 | } 103 | return false; 104 | } 105 | 106 | public static boolean isPureAscii(String v) { 107 | return Charset.forName("US-ASCII").newEncoder().canEncode(v); 108 | } 109 | 110 | public static String classSigToRawFullName(String clsSig) { 111 | if (clsSig != null && clsSig.startsWith("L") && clsSig.endsWith(";")) { 112 | clsSig = clsSig.substring(1, clsSig.length() - 1).replace("/", "."); 113 | } 114 | return clsSig; 115 | } 116 | 117 | public static String classSigToRawShortName(String clsSig) { 118 | if (clsSig == null) { 119 | return null; 120 | } 121 | if (clsSig.startsWith("L") && clsSig.endsWith(";")) { 122 | clsSig = clsSig.substring(1, clsSig.length() - 1).replace("/", "."); 123 | } 124 | return clsSig.substring(clsSig.lastIndexOf('.') + 1); 125 | } 126 | 127 | public static boolean isJVMSig(String sig) { 128 | if (sig == null) { 129 | return false; 130 | } 131 | if(sig.matches("^[a-zA-Z_\\$<][\\w\\$>]*\\(\\[?[a-zA-Z0-9\\$_/\\[\\];]*\\)([BCDFIJSVZ]|\\[?[L\\[a-zA-Z0-9\\$_/]+;)$")) { 132 | return true; 133 | } 134 | return false; 135 | } 136 | 137 | public static String stringWrapper(EncodedValue encodedValue) { 138 | if (encodedValue != null && encodedValue.getType() == EncodedType.ENCODED_STRING) { 139 | return (String) encodedValue.getValue(); 140 | } else if (encodedValue == null) { 141 | return null; 142 | } else { 143 | return encodedValue.toString(); 144 | } 145 | } 146 | 147 | public static String removeExtension(String filename) { 148 | int lastDotIndex = filename.lastIndexOf('.'); 149 | if (lastDotIndex > 0) { 150 | return filename.substring(0, lastDotIndex); 151 | } 152 | return filename; 153 | } 154 | 155 | public static String getFileExtension(File file) { 156 | return getFileExtension(file.getName()); 157 | } 158 | 159 | public static String getFileExtension(String name) { 160 | int lastDotIndex = name.lastIndexOf('.'); 161 | if (lastDotIndex > 0) { 162 | return name.substring(lastDotIndex + 1); 163 | } 164 | return ""; 165 | } 166 | 167 | public static ZipFile inputSteamToZipFile(InputStream is) throws IOException { 168 | File zipTemp = File.createTempFile("nested-", ".zip"); 169 | zipTemp.deleteOnExit(); 170 | 171 | FileOutputStream fos = new FileOutputStream(zipTemp); 172 | byte[] buffer = new byte[4096]; 173 | int bytesRead; 174 | while ((bytesRead = is.read(buffer)) != -1) { 175 | fos.write(buffer, 0, bytesRead); 176 | } 177 | 178 | return new ZipFile(zipTemp); 179 | } 180 | 181 | public static String convertToXML(Map map, String rootElementName) throws ParserConfigurationException, Exception { 182 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 183 | DocumentBuilder builder = factory.newDocumentBuilder(); 184 | Document document = builder.newDocument(); 185 | 186 | Element root = document.createElement(rootElementName); 187 | document.appendChild(root); 188 | 189 | appendMapToElement(document, root, map); 190 | 191 | return transformDocumentToString(document); 192 | } 193 | 194 | private static void appendMapToElement(Document document, Element parent, Map map) { 195 | for (Map.Entry entry : map.entrySet()) { 196 | Element element = document.createElement(entry.getKey()); 197 | if (entry.getValue() instanceof Map) { 198 | appendMapToElement(document, element, (Map) entry.getValue()); 199 | } else if (entry.getValue() instanceof List) { 200 | appendListToElement(document, element, (List) entry.getValue()); 201 | } else { 202 | element.appendChild(document.createTextNode(entry.getValue().toString())); 203 | } 204 | parent.appendChild(element); 205 | } 206 | } 207 | 208 | private static void appendListToElement(Document document, Element parent, List list) { 209 | for (Object item : list) { 210 | Element itemElement = document.createElement("item"); 211 | if (item instanceof Map) { 212 | appendMapToElement(document, itemElement, (Map) item); 213 | } else { 214 | itemElement.appendChild(document.createTextNode(item.toString())); 215 | } 216 | parent.appendChild(itemElement); 217 | } 218 | } 219 | 220 | private static String transformDocumentToString(Document document) throws TransformerException { 221 | TransformerFactory transformerFactory = TransformerFactory.newInstance(); 222 | Transformer transformer = transformerFactory.newTransformer(); 223 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 224 | transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); 225 | 226 | StringWriter writer = new StringWriter(); 227 | transformer.transform(new DOMSource(document), new StreamResult(writer)); 228 | return writer.toString(); 229 | } 230 | 231 | public static boolean isValidXmlContent(String content) { 232 | if (content == null || content.isEmpty()) { 233 | return false; 234 | } 235 | String trimmedContent = content.trim(); 236 | return trimmedContent.startsWith(" annotationValues, 23 | List localVars, 24 | int methodArg, 25 | ArgType argType, 26 | RootNode rootNode) throws Exception { 27 | switch (annotationClass) { 28 | case Constants.Micronaut.QUERY_VALUE -> { 29 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value")), paramInfo, localVars, methodArg); 30 | if (annotationValues.get("defaultValue") != null) { 31 | paramInfo.setDefaultValue(Helpers.stringWrapper(annotationValues.get("defaultValue"))); 32 | } 33 | AnnotationUtils.processQueryParameter(request, paramName, paramInfo.getDefaultValue(), argType, rootNode); 34 | return ArgProcessingState.PARAMETER_CREATED; 35 | } 36 | case Constants.Micronaut.PATH_VARIABLE -> { 37 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "name")), paramInfo, localVars, methodArg); 38 | if (paramName != null) { 39 | request.addPathParameter(paramName); 40 | } 41 | return ArgProcessingState.PARAMETER_CREATED; 42 | } 43 | case Constants.Micronaut.HEADER -> { 44 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value")), paramInfo, localVars, methodArg); 45 | if (annotationValues.get("defaultValue") != null) { 46 | paramInfo.setDefaultValue(Helpers.stringWrapper(annotationValues.get("defaultValue"))); 47 | } 48 | AnnotationUtils.processHeader(request, paramName, paramInfo.getDefaultValue()); 49 | return ArgProcessingState.PARAMETER_CREATED; 50 | } 51 | case Constants.Micronaut.PART -> { 52 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value")), paramInfo, localVars, methodArg); 53 | AnnotationUtils.processPart(request, paramName); 54 | return ArgProcessingState.PARAMETER_CREATED; 55 | } 56 | case Constants.Micronaut.BODY -> { 57 | String paramName = AnnotationUtils.getParamName(null, paramInfo, localVars, methodArg); 58 | AnnotationUtils.processArbitraryBodyParameter(request, paramName, argType, rootNode); 59 | return ArgProcessingState.PARAMETER_CREATED; 60 | } 61 | case Constants.Micronaut.COOKIE_VALUE -> { 62 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value")), paramInfo, localVars, methodArg); 63 | if (annotationValues.get("defaultValue") != null) { 64 | paramInfo.setDefaultValue(Helpers.stringWrapper(annotationValues.get("defaultValue"))); 65 | } 66 | AnnotationUtils.processCookieParameter(request, paramName, paramInfo.getDefaultValue()); 67 | return ArgProcessingState.PARAMETER_CREATED; 68 | } 69 | default -> { 70 | return ArgProcessingState.NOT_PROCESSED; 71 | } 72 | } 73 | } 74 | 75 | @Override 76 | public boolean processMethodAnnotations(MultiHTTPRequest request, 77 | String annotationClass, 78 | Map annotationValues, 79 | RootNode rn) { 80 | switch (annotationClass) { 81 | case Constants.Micronaut.TRACE, Constants.Micronaut.HEAD, Constants.Micronaut.GET, Constants.Micronaut.POST, Constants.Micronaut.PUT, Constants.Micronaut.DELETE, Constants.Micronaut.OPTIONS, Constants.Micronaut.PATCH -> { 82 | request.addAdditionalInformation("Micronaut Method"); 83 | request.setMethod(Helpers.classSigToRawShortName(annotationClass).toUpperCase()); 84 | processCommonHttpMethodAnnotations(request, annotationValues); 85 | return true; 86 | } 87 | case Constants.Micronaut.CUSTOM_HTTP_METHOD -> { 88 | request.addAdditionalInformation("Micronaut Method"); 89 | if (annotationValues.get("method") != null) { 90 | request.setMethod(Helpers.stringWrapper(annotationValues.get("method"))); 91 | } 92 | processCommonHttpMethodAnnotations(request, annotationValues); 93 | return true; 94 | } 95 | case Constants.Micronaut.HEADER -> { 96 | processHeaderAnnotation(request, annotationValues); 97 | return true; 98 | } 99 | case Constants.Micronaut.HEADERS -> { 100 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 101 | if (value != null) { 102 | ArrayList headers = (ArrayList) value.getValue(); 103 | if (headers != null) { 104 | for (EncodedValue header : headers) { 105 | processHeaderAnnotation(request, ((JadxAnnotation) header.getValue()).getValues()); 106 | } 107 | } 108 | } 109 | return true; 110 | } 111 | case Constants.Micronaut.CONSUMES -> { 112 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "a")); 113 | if (value != null) { 114 | ArrayList consumesList = (ArrayList) value.getValue(); 115 | if (!consumesList.isEmpty()) { 116 | AnnotationUtils.processContentTypeFromList(request, consumesList); 117 | } 118 | } 119 | return true; 120 | } 121 | default -> { 122 | return false; 123 | } 124 | } 125 | } 126 | 127 | private void processHeaderAnnotation(MultiHTTPRequest request, Map annotationValues) { 128 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("name")); 129 | if (value != null) { 130 | String headerName = Helpers.stringWrapper(value); 131 | String headerValue = ""; 132 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("value"))) != null) { 133 | headerValue = Helpers.stringWrapper(value); 134 | } 135 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("defaultValue"))) != null) { 136 | headerValue = Helpers.stringWrapper(value); 137 | } 138 | request.putHeader(headerName, headerValue.equals("") ? headerName : headerValue); 139 | } 140 | } 141 | 142 | private void processCommonHttpMethodAnnotations(MultiHTTPRequest request, Map annotationValues) { 143 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("uris")); 144 | if (value != null) { 145 | ArrayList pathlist = (ArrayList) value.getValue(); 146 | List controllerPaths = new ArrayList(); 147 | for (EncodedValue path : pathlist) { 148 | controllerPaths.add(Helpers.stringWrapper(path)); 149 | } 150 | request.setPaths(controllerPaths); 151 | } 152 | value = AnnotationUtils.getValue(annotationValues, List.of("uri", "value")); 153 | if (value != null) { 154 | request.setPath(Helpers.stringWrapper(value), false); 155 | } 156 | value = AnnotationUtils.getValue(annotationValues, List.of("consumes", "processes")); 157 | if (value != null) { 158 | ArrayList consumesList = (ArrayList) value.getValue(); 159 | if (consumesList != null && !consumesList.isEmpty()) { 160 | AnnotationUtils.processContentTypeFromList(request, consumesList); 161 | } 162 | } 163 | } 164 | 165 | @Override 166 | public boolean processClassAnnotations(MultiHTTPRequest request, 167 | String annotationClass, 168 | Map annotationValues, 169 | String globalBasePath, 170 | String className, 171 | RootNode rn) { 172 | switch (annotationClass) { 173 | case Constants.Micronaut.HEADER -> { 174 | processHeaderAnnotation(request, annotationValues); 175 | return false; 176 | } 177 | case Constants.Micronaut.HEADERS -> { 178 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 179 | if (value != null) { 180 | ArrayList headers = (ArrayList) value.getValue(); 181 | if (headers != null) { 182 | for (EncodedValue header : headers) { 183 | processHeaderAnnotation(request, ((JadxAnnotation) header.getValue()).getValues()); 184 | } 185 | } 186 | } 187 | return false; 188 | } 189 | case Constants.Micronaut.CONTROLLER -> { 190 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 191 | if (value != null) { 192 | String classPath = Helpers.stringWrapper(value); 193 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 194 | request.setBasePaths(List.of(fullPath)); 195 | } 196 | value = annotationValues.get("consumes"); 197 | if (value != null) { 198 | ArrayList consumesList = (ArrayList) value.getValue(); 199 | if (consumesList != null && !consumesList.isEmpty()) { 200 | AnnotationUtils.processContentTypeFromList(request, consumesList); 201 | } 202 | } 203 | return false; 204 | } 205 | case Constants.Micronaut.CLIENT -> { 206 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "path")); 207 | if (value != null) { 208 | String classPath = Helpers.stringWrapper(value); 209 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 210 | request.setBasePaths(List.of(fullPath)); 211 | } 212 | return false; 213 | } 214 | 215 | default -> { 216 | return false; 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/Main.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan; 2 | 3 | import jadx.api.args.UserRenamesMappingsMode; 4 | import jadx.api.impl.NoOpCodeCache; 5 | import jadx.api.impl.SimpleCodeWriter; 6 | import jadx.api.JadxArgs; 7 | import jadx.api.JadxDecompiler; 8 | import jadx.api.impl.InMemoryCodeCache; 9 | import jadx.plugins.mappings.RenameMappingsOptions; 10 | import java.io.File; 11 | import java.io.FileWriter; 12 | import java.io.IOException; 13 | import java.io.PrintWriter; 14 | import java.net.MalformedURLException; 15 | import java.net.URI; 16 | import java.net.URISyntaxException; 17 | import java.net.URL; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.nio.file.Paths; 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import org.apache.commons.cli.CommandLine; 25 | import org.apache.commons.cli.CommandLineParser; 26 | import org.apache.commons.cli.DefaultParser; 27 | import org.apache.commons.cli.HelpFormatter; 28 | import org.apache.commons.cli.Option; 29 | import org.apache.commons.cli.Options; 30 | import org.apache.commons.cli.ParseException; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | import ru.blackfan.bfscan.cli.CommandLineResult; 34 | import ru.blackfan.bfscan.helpers.Helpers; 35 | import ru.blackfan.bfscan.jadx.JadxBasePluginLoader; 36 | import ru.blackfan.bfscan.parsing.constants.ConstantsProcessor; 37 | import ru.blackfan.bfscan.parsing.httprequests.HTTPRequestProcessor; 38 | 39 | public class Main { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(Main.class); 42 | 43 | public static void main(String[] args) { 44 | Options options = createOptions(); 45 | CommandLineParser parser = new DefaultParser(); 46 | HelpFormatter formatter = new HelpFormatter(); 47 | 48 | try { 49 | final Path mappingPath; 50 | CommandLineResult cmdResult = parseCommandLine(parser, options, args); 51 | if (cmdResult.renameMappingFile != null) { 52 | mappingPath = Paths.get(cmdResult.renameMappingFile); 53 | if (!Files.exists(mappingPath) || !Files.isRegularFile(mappingPath) || !Files.isReadable(mappingPath)) { 54 | throw new IllegalArgumentException("Mapping file is not available or not readable: " + cmdResult.renameMappingFile); 55 | } 56 | } else { 57 | mappingPath = null; 58 | } 59 | 60 | cmdResult.inputFiles.parallelStream().forEach(inputFile -> { 61 | try { 62 | processInputFile(inputFile, mappingPath, cmdResult); 63 | } catch (Throwable e) { 64 | logger.error("Error processing file " + inputFile.getName(), e); 65 | } 66 | }); 67 | 68 | } catch (ParseException e) { 69 | handleError("Command line parsing error: " + e.getMessage(), e, formatter, options); 70 | } catch (Exception e) { 71 | handleError("Fatal error", e, formatter, options); 72 | } 73 | } 74 | 75 | private static void processInputFile(File inputFile, Path mappingPath, CommandLineResult cmdResult) { 76 | String reportUrlsPaths = inputFile.getAbsoluteFile().getParent() + File.separator + Helpers.removeExtension(inputFile.getName()) + "_urls.md"; 77 | String reportSecrets = inputFile.getAbsoluteFile().getParent() + File.separator + Helpers.removeExtension(inputFile.getName()) + "_secrets.md"; 78 | String reportMethods = inputFile.getAbsoluteFile().getParent() + File.separator + Helpers.removeExtension(inputFile.getName()) + "_methods.md"; 79 | String reportOpenApi = inputFile.getAbsoluteFile().getParent() + File.separator + Helpers.removeExtension(inputFile.getName()) + "_openapi.yaml"; 80 | String reportSearch = inputFile.getAbsoluteFile().getParent() + File.separator + Helpers.removeExtension(inputFile.getName()) + "_search.md"; 81 | 82 | logger.info("Processing: " + inputFile.getName()); 83 | 84 | try { 85 | PrintWriter writerSearch = cmdResult.searchString != null ? new PrintWriter(new FileWriter(reportSearch)) : null; 86 | 87 | JadxDecompiler jadx = initializeJadx(inputFile, mappingPath); 88 | 89 | if (List.of("a", "all", "s", "secrets", "secret").contains(cmdResult.mode)) { 90 | PrintWriter writerSecrets = new PrintWriter(new FileWriter(reportSecrets)); 91 | PrintWriter writerUrlsPaths = new PrintWriter(new FileWriter(reportUrlsPaths)); 92 | ConstantsProcessor constantsProcessor = new ConstantsProcessor(jadx, writerUrlsPaths, writerSecrets, writerSearch); 93 | constantsProcessor.processConstants(cmdResult.searchString); 94 | writerSecrets.close(); 95 | writerUrlsPaths.close(); 96 | if (writerSearch != null) { 97 | writerSearch.close(); 98 | } 99 | logger.info("URLs saved to: " + reportUrlsPaths); 100 | logger.info("Secrets saved to: " + reportSecrets); 101 | } 102 | 103 | if (List.of("a", "all", "h", "http").contains(cmdResult.mode)) { 104 | PrintWriter writerMethods = new PrintWriter(new FileWriter(reportMethods)); 105 | PrintWriter writerOpenApi = new PrintWriter(new FileWriter(reportOpenApi)); 106 | HTTPRequestProcessor processor = new HTTPRequestProcessor(jadx, cmdResult.apiUrl, cmdResult.minifiedAnnotationsSupport, writerMethods, writerOpenApi); 107 | processor.processHttpMethods(); 108 | writerMethods.close(); 109 | writerOpenApi.close(); 110 | logger.info("Raw http methods saved to: " + reportMethods); 111 | logger.info("OpenAPI spec saved to: " + reportOpenApi); 112 | } 113 | 114 | if (cmdResult.searchString != null) { 115 | logger.info("Search result saved to: " + reportSearch); 116 | } 117 | 118 | } catch (IOException e) { 119 | throw new RuntimeException("Error writing output files for " + inputFile.getName(), e); 120 | } catch (Exception e) { 121 | throw new RuntimeException("Error processing file " + inputFile.getName(), e); 122 | } 123 | } 124 | 125 | private static Options createOptions() { 126 | Options options = new Options(); 127 | 128 | Option urlOption = new Option("u", true, "API base url (http://localhost/api/)"); 129 | urlOption.setRequired(false); 130 | urlOption.setArgName("url"); 131 | 132 | Option verboseOption = new Option("v", true, "Log level (off, error, warn, info, debug, trace)"); 133 | verboseOption.setRequired(false); 134 | verboseOption.setArgName("verbose"); 135 | 136 | Option searchOption = new Option("s", true, "Search string"); 137 | searchOption.setRequired(false); 138 | searchOption.setArgName("searchString"); 139 | 140 | Option modeOption = new Option("m", true, "Mode ([a]ll, [s]ecrets, [h]ttp), default: all"); 141 | modeOption.setRequired(false); 142 | modeOption.setArgName("mode"); 143 | 144 | Option renameOption = new Option("r", true, "Rename mapping file"); 145 | renameOption.setRequired(false); 146 | renameOption.setArgName("mappingFile"); 147 | 148 | Option minifiedAnnotationsSupport = new Option("ma", true, "Minified or unknown annotations support (yes, no), default: yes"); 149 | minifiedAnnotationsSupport.setRequired(false); 150 | minifiedAnnotationsSupport.setArgName("minifiedAnnotationsSupport"); 151 | 152 | options.addOption(urlOption); 153 | options.addOption(verboseOption); 154 | options.addOption(searchOption); 155 | options.addOption(modeOption); 156 | options.addOption(renameOption); 157 | options.addOption(minifiedAnnotationsSupport); 158 | return options; 159 | } 160 | 161 | private static CommandLineResult parseCommandLine(CommandLineParser parser, Options options, String[] args) 162 | throws ParseException, MalformedURLException, URISyntaxException { 163 | CommandLine commandLine = parser.parse(options, args); 164 | List cliArgs = commandLine.getArgList(); 165 | if (cliArgs.isEmpty()) { 166 | throw new ParseException("Specify at least one input file (APK, APKM, XAPK, JAR, WAR or DEX)"); 167 | } 168 | 169 | List inputFiles = new ArrayList<>(); 170 | for (String arg : cliArgs) { 171 | File file = new File(arg); 172 | validateInputFile(file); 173 | inputFiles.add(file); 174 | } 175 | 176 | URI apiUrl; 177 | if (commandLine.hasOption("u")) { 178 | apiUrl = new URL(commandLine.getOptionValue("u")).toURI(); 179 | } else { 180 | apiUrl = new URL("http://localhost/").toURI(); 181 | } 182 | 183 | if (commandLine.hasOption("v")) { 184 | System.setProperty("org.slf4j.simpleLogger.log.jadx", commandLine.getOptionValue("v")); 185 | System.setProperty("org.slf4j.simpleLogger.log.ru.blackfan", commandLine.getOptionValue("v")); 186 | } 187 | 188 | String searchString = null; 189 | if (commandLine.hasOption("s")) { 190 | searchString = commandLine.getOptionValue("s"); 191 | } 192 | 193 | String mode; 194 | if (commandLine.hasOption("m")) { 195 | mode = commandLine.getOptionValue("m"); 196 | } else { 197 | mode = "all"; 198 | } 199 | 200 | String renameMappingPath = null; 201 | if (commandLine.hasOption("r")) { 202 | renameMappingPath = commandLine.getOptionValue("r"); 203 | } 204 | 205 | boolean minifiedAnnotationsSupport = true; 206 | if (commandLine.hasOption("ma")) { 207 | switch (commandLine.getOptionValue("ma")) { 208 | case "no" -> minifiedAnnotationsSupport = false; 209 | case "yes" -> minifiedAnnotationsSupport = true; 210 | default -> throw new ParseException("Incorrect value in -ma option"); 211 | } 212 | } 213 | 214 | return new CommandLineResult(inputFiles, apiUrl, searchString, mode, renameMappingPath, minifiedAnnotationsSupport); 215 | } 216 | 217 | private static JadxDecompiler initializeJadx(File inputFile, Path mappingPath) { 218 | JadxArgs jadxArgs = new JadxArgs(); 219 | jadxArgs.setInputFile(inputFile); 220 | jadxArgs.setCodeCache(new InMemoryCodeCache()); 221 | jadxArgs.setReplaceConsts(false); 222 | jadxArgs.setDebugInfo(true); 223 | jadxArgs.setPluginLoader(new JadxBasePluginLoader()); 224 | jadxArgs.setCodeWriterProvider(SimpleCodeWriter::new); 225 | jadxArgs.setMoveInnerClasses(false); 226 | jadxArgs.setShowInconsistentCode(true); 227 | jadxArgs.getPluginOptions().put("jadx.plugins.kotlin.metadata.fields", "true"); 228 | if (mappingPath != null) { 229 | jadxArgs.setUserRenamesMappingsPath(mappingPath); 230 | jadxArgs.setUserRenamesMappingsMode(UserRenamesMappingsMode.READ); 231 | jadxArgs.getPluginOptions().put(RenameMappingsOptions.FORMAT_OPT, "AUTO"); 232 | jadxArgs.getPluginOptions().put(RenameMappingsOptions.INVERT_OPT, "no"); 233 | } 234 | JadxDecompiler jadx = new JadxDecompiler(jadxArgs); 235 | jadx.load(); 236 | return jadx; 237 | } 238 | 239 | private static void validateInputFile(File file) throws IllegalArgumentException { 240 | if (!file.exists()) { 241 | throw new IllegalArgumentException("File does not exist: " + file.getPath()); 242 | } 243 | if (!file.canRead()) { 244 | throw new IllegalArgumentException("Cannot read file: " + file.getPath()); 245 | } 246 | String ext = Helpers.getFileExtension(file); 247 | if (!Arrays.asList("apk", "apkm", "xapk", "jar", "war", "dex", "zip").contains(ext.toLowerCase())) { 248 | throw new IllegalArgumentException("Unsupported file type: " + ext); 249 | } 250 | } 251 | 252 | private static void handleError(String message, Throwable error, HelpFormatter formatter, Options options) { 253 | logger.error(message, error); 254 | formatter.setWidth(200); 255 | formatter.printHelp("java -jar bfscan.jar <...>", options, true); 256 | System.exit(1); 257 | } 258 | 259 | } 260 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/SpringProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.ILocalVar; 5 | import jadx.api.plugins.input.data.impl.JadxFieldRef; 6 | import jadx.core.dex.instructions.args.ArgType; 7 | import jadx.core.dex.nodes.RootNode; 8 | import java.util.ArrayList; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Map; 12 | import ru.blackfan.bfscan.helpers.Helpers; 13 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 14 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 15 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 16 | 17 | public class SpringProcessor implements AnnotationProcessor { 18 | 19 | @Override 20 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 21 | ParameterInfo paramInfo, 22 | String annotationClass, 23 | Map annotationValues, 24 | List localVars, 25 | int methodArg, 26 | ArgType argType, 27 | RootNode rootNode) throws Exception { 28 | switch (annotationClass) { 29 | case Constants.Spring.PARAM_HEADER -> { 30 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "name")), paramInfo, localVars, methodArg); 31 | if (annotationValues.get("defaultValue") != null) { 32 | paramInfo.setDefaultValue(Helpers.stringWrapper(annotationValues.get("defaultValue"))); 33 | } 34 | AnnotationUtils.processHeader(request, paramName, paramInfo.getDefaultValue()); 35 | return ArgProcessingState.PARAMETER_CREATED; 36 | } 37 | case Constants.Spring.PARAM_PATH -> { 38 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "name")), paramInfo, localVars, methodArg); 39 | if (paramName != null) { 40 | request.addPathParameter(paramName); 41 | } 42 | return ArgProcessingState.PARAMETER_CREATED; 43 | } 44 | case Constants.Spring.PARAM_PART -> { 45 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "name")), paramInfo, localVars, methodArg); 46 | AnnotationUtils.processPart(request, paramName); 47 | return ArgProcessingState.PARAMETER_CREATED; 48 | } 49 | case Constants.Spring.PARAM_BODY, Constants.Spring.PARAM_MODEL -> { 50 | String paramName = AnnotationUtils.getParamName(null, paramInfo, localVars, methodArg); 51 | AnnotationUtils.processArbitraryBodyParameter(request, paramName, argType, rootNode); 52 | return ArgProcessingState.PARAMETER_CREATED; 53 | } 54 | case Constants.Spring.BIND_PARAM, Constants.Spring.REQUEST_PARAMETER -> { 55 | Object paramValue; 56 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "name", "a")), paramInfo, localVars, methodArg); 57 | if (!argType.isPrimitive() && AnnotationUtils.isMultipartObject(argType.getObject())) { 58 | request.setEncType("multipart"); 59 | } 60 | if (annotationValues.get("defaultValue") != null) { 61 | paramValue = Helpers.stringWrapper(annotationValues.get("defaultValue")); 62 | } else if (!paramInfo.getDefaultValue().isEmpty()) { 63 | paramValue = paramInfo.getDefaultValue(); 64 | } else { 65 | paramValue = AnnotationUtils.argTypeToValue(paramName, argType, rootNode, new HashSet<>(), true); 66 | } 67 | if (paramName != null && paramValue != null) { 68 | AnnotationUtils.appendParametersToRequest(request, Map.of(paramName, paramValue)); 69 | return ArgProcessingState.PARAMETER_CREATED; 70 | } 71 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 72 | } 73 | case Constants.Spring.PARAM_COOKIE -> { 74 | String paramName = AnnotationUtils.getParamName(AnnotationUtils.getValue(annotationValues, List.of("value", "name")), paramInfo, localVars, methodArg); 75 | if (annotationValues.get("defaultValue") != null) { 76 | paramInfo.setDefaultValue(Helpers.stringWrapper(annotationValues.get("defaultValue"))); 77 | } 78 | AnnotationUtils.processCookieParameter(request, paramName, paramInfo.getDefaultValue()); 79 | return ArgProcessingState.PARAMETER_CREATED; 80 | } 81 | default -> { 82 | return ArgProcessingState.NOT_PROCESSED; 83 | } 84 | } 85 | } 86 | 87 | @Override 88 | public boolean processMethodAnnotations(MultiHTTPRequest request, 89 | String annotationClass, 90 | Map annotationValues, 91 | RootNode rn) { 92 | switch (annotationClass) { 93 | case Constants.Spring.REQUEST_MAPPING, Constants.Spring.GET_MAPPING, Constants.Spring.DELETE_MAPPING, Constants.Spring.PATCH_MAPPING, Constants.Spring.PUT_MAPPING, Constants.Spring.POST_MAPPING -> { 94 | request.addAdditionalInformation("Spring Method"); 95 | if (!annotationClass.equals(Constants.Spring.REQUEST_MAPPING)) { 96 | String alias = Helpers.classSigToRawShortName(annotationClass); 97 | request.setMethod(alias.replace("Mapping", "").toUpperCase()); 98 | } else { 99 | processHttpMethod(request, annotationValues); 100 | } 101 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value", "path")); 102 | if (value != null) { 103 | ArrayList pathlist = (ArrayList) value.getValue(); 104 | List controllerPaths = new ArrayList(); 105 | for (EncodedValue path : pathlist) { 106 | controllerPaths.add(Helpers.stringWrapper(path)); 107 | } 108 | request.setPaths(controllerPaths); 109 | } 110 | processConsumes(request, annotationValues); 111 | processParams(request, annotationValues); 112 | processHeaders(request, annotationValues); 113 | return true; 114 | } 115 | case Constants.Spring.BEAN -> { 116 | EncodedValue beanNameValue = AnnotationUtils.getValue(annotationValues, List.of("value", "name")); 117 | if (beanNameValue != null) { 118 | Object valueObj = beanNameValue.getValue(); 119 | List beanPaths = new ArrayList(); 120 | 121 | if (valueObj instanceof ArrayList) { 122 | ArrayList beanNameList = (ArrayList) valueObj; 123 | for (EncodedValue beanNameEncoded : beanNameList) { 124 | String beanName = Helpers.stringWrapper(beanNameEncoded); 125 | if (beanName != null && beanName.startsWith("/")) { 126 | beanPaths.add(beanName); 127 | } 128 | } 129 | } else { 130 | String beanName = Helpers.stringWrapper(beanNameValue); 131 | if (beanName != null && beanName.startsWith("/")) { 132 | beanPaths.add(beanName); 133 | } 134 | } 135 | 136 | if (!beanPaths.isEmpty()) { 137 | request.addAdditionalInformation("Spring BeanNameUrlHandlerMapping"); 138 | request.setPaths(beanPaths); 139 | return true; 140 | } 141 | } 142 | return false; 143 | } 144 | default -> { 145 | return false; 146 | } 147 | } 148 | } 149 | 150 | @Override 151 | public boolean processClassAnnotations(MultiHTTPRequest request, 152 | String annotationClass, 153 | Map annotationValues, 154 | String globalBasePath, 155 | String className, 156 | RootNode rn) { 157 | switch (annotationClass) { 158 | case Constants.Spring.REQUEST_MAPPING -> { 159 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("path", "value")); 160 | if (value != null) { 161 | ArrayList basePathList = (ArrayList) value.getValue(); 162 | List basePaths = new ArrayList(); 163 | for (EncodedValue path : basePathList) { 164 | String classPath = Helpers.stringWrapper(path); 165 | if (!globalBasePath.equals("/")) { 166 | classPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 167 | } 168 | basePaths.add(classPath); 169 | } 170 | request.setBasePaths(basePaths); 171 | } 172 | processHttpMethod(request, annotationValues); 173 | processConsumes(request, annotationValues); 174 | processParams(request, annotationValues); 175 | processHeaders(request, annotationValues); 176 | return false; 177 | } 178 | default -> { 179 | return false; 180 | } 181 | } 182 | } 183 | 184 | private void processHttpMethod(MultiHTTPRequest request, Map annotationValues) { 185 | EncodedValue value = annotationValues.get("method"); 186 | if (value != null) { 187 | ArrayList methodlist = (ArrayList) value.getValue(); 188 | List controllerMethods = new ArrayList(); 189 | for (EncodedValue method : methodlist) { 190 | controllerMethods.add(((JadxFieldRef) method.getValue()).getName()); 191 | } 192 | request.setMethods(controllerMethods); 193 | } 194 | } 195 | 196 | private void processConsumes(MultiHTTPRequest request, Map annotationValues) { 197 | EncodedValue value = annotationValues.get("consumes"); 198 | if (value != null) { 199 | ArrayList consumesList = (ArrayList) value.getValue(); 200 | if (consumesList != null && !consumesList.isEmpty()) { 201 | AnnotationUtils.processContentTypeFromList(request, consumesList); 202 | } 203 | } 204 | } 205 | 206 | private void processParams(MultiHTTPRequest request, Map annotationValues) { 207 | EncodedValue value = annotationValues.get("params"); 208 | if (value != null) { 209 | ArrayList paramlist = (ArrayList) value.getValue(); 210 | for (EncodedValue param : paramlist) { 211 | String p = Helpers.stringWrapper(param); 212 | if (p.contains("=")) { 213 | String[] parts = p.split("=", 2); 214 | request.putQueryParameter(parts[0], parts[1]); 215 | } else { 216 | request.putQueryParameter(p, p); 217 | } 218 | } 219 | } 220 | } 221 | 222 | private void processHeaders(MultiHTTPRequest request, Map annotationValues) { 223 | EncodedValue value = annotationValues.get("headers"); 224 | if (value != null) { 225 | ArrayList headerlist = (ArrayList) value.getValue(); 226 | for (EncodedValue header : headerlist) { 227 | String h = Helpers.stringWrapper(header); 228 | if (h.contains("=")) { 229 | String[] parts = h.split("=", 2); 230 | request.putHeader(parts[0].trim(), parts[1].trim()); 231 | } else { 232 | request.putHeader(h, h); 233 | } 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/constants/ConstantsProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.constants; 2 | 3 | import jadx.api.JadxDecompiler; 4 | import jadx.api.JavaClass; 5 | import jadx.api.ResourceFile; 6 | import jadx.api.ResourcesLoader; 7 | import jadx.api.ResourceType; 8 | import jadx.core.utils.exceptions.JadxException; 9 | import jadx.core.xmlgen.ResContainer; 10 | import java.io.InputStream; 11 | import java.io.IOException; 12 | import java.io.PrintWriter; 13 | import java.util.Arrays; 14 | import java.util.Collections; 15 | import java.util.concurrent.atomic.AtomicReference; 16 | import java.util.HashSet; 17 | import java.util.LinkedHashMap; 18 | import java.util.LinkedHashSet; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.regex.Matcher; 22 | import java.util.regex.Pattern; 23 | import java.util.Set; 24 | import java.util.zip.ZipEntry; 25 | import java.util.zip.ZipFile; 26 | import javax.xml.parsers.DocumentBuilderFactory; 27 | import javax.xml.parsers.ParserConfigurationException; 28 | import javax.xml.XMLConstants; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | import ru.blackfan.bfscan.config.ConfigLoader; 32 | import ru.blackfan.bfscan.helpers.Helpers; 33 | import ru.blackfan.bfscan.helpers.KeyValuePair; 34 | import ru.blackfan.bfscan.parsing.constants.file.ApkResources; 35 | import ru.blackfan.bfscan.parsing.constants.file.Class; 36 | import ru.blackfan.bfscan.parsing.constants.file.Properties; 37 | import ru.blackfan.bfscan.parsing.constants.file.Xml; 38 | import ru.blackfan.bfscan.parsing.constants.file.Yml; 39 | 40 | public class ConstantsProcessor { 41 | 42 | private static final Logger logger = LoggerFactory.getLogger(ConstantsProcessor.class); 43 | 44 | private final JadxDecompiler jadx; 45 | private final PrintWriter writerUrlsPaths; 46 | private final PrintWriter writerSecrets; 47 | private final PrintWriter writerSearch; 48 | private static final double ENTROPY_THRESHOLD = 4.1; 49 | private static final double ENTROPY_THRESHOLD_MIN = 3.9; 50 | private static final List SECRET_KEYWORDS = Arrays.asList("api", "secret", "token", "auth", "pass", "password", "pwd", "hash", "salt", "crypt", "cert", "sign", "credential"); 51 | private static final Pattern INVALID_CONTROL_CHARS_PATTERN = Pattern.compile("[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F]", Pattern.DOTALL); 52 | private static final Pattern URL_PATTERN = Pattern.compile("(\\bhttps?)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-A-Za-z0-9+&@#/%=~_|]"); 53 | private static final Pattern QUOTE_PATTERN = Pattern.compile("['\"`](.*?)['\"`]"); 54 | private final Set urls = new LinkedHashSet<>(); 55 | private final Set paths = new LinkedHashSet<>(); 56 | private final Set searchResults = new LinkedHashSet<>(); 57 | private final Map> secrets = new LinkedHashMap<>(); 58 | private final DocumentBuilderFactory factory; 59 | public static final String EXCLUDED_SECRETS_REGEXP = ConfigLoader.getExcludedSecretsRegexp(); 60 | public static final String EXCLUDED_LINKS_REGEXP = ConfigLoader.getExcludedLinksRegexp(); 61 | 62 | public ConstantsProcessor(JadxDecompiler jadx, PrintWriter writerUrlsPaths, PrintWriter writerSecrets, PrintWriter writerSearch) throws ParserConfigurationException { 63 | this.jadx = jadx; 64 | this.writerUrlsPaths = writerUrlsPaths; 65 | this.writerSecrets = writerSecrets; 66 | this.writerSearch = writerSearch; 67 | 68 | this.factory = DocumentBuilderFactory.newInstance(); 69 | this.factory.setValidating(false); 70 | this.factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 71 | this.factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 72 | this.factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 73 | this.factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 74 | } 75 | 76 | public void checkConstants(String path, Set keyValuePairs, String searchString) { 77 | if (keyValuePairs != null) { 78 | for (KeyValuePair pair : keyValuePairs) { 79 | String value = pair.getValue(); 80 | if (Helpers.isValidURI(value) && !value.matches(EXCLUDED_LINKS_REGEXP)) { 81 | logger.debug("Found URI " + value); 82 | urls.add(value); 83 | } 84 | if (Helpers.isValidUrlPath(value)) { 85 | logger.debug("Found path " + value); 86 | paths.add(value); 87 | } 88 | if (!urls.contains(value) && !paths.contains(value)) { 89 | String secretReason = getSecretReason(pair.getKey(), value); 90 | if (secretReason != null) { 91 | String key = path + "#" + pair.getKey() + "(" + secretReason + ")"; 92 | logger.debug("Found secret " + value); 93 | if (secrets.containsKey(value)) { 94 | secrets.get(value).add(key); 95 | } else { 96 | secrets.put(value, new HashSet<>(Arrays.asList(key))); 97 | } 98 | } 99 | } 100 | if (searchString != null) { 101 | if (value.contains(searchString)) { 102 | String key = path + "#" + pair.getKey(); 103 | logger.debug("Found search string " + value); 104 | searchResults.add("* " + key + "\n```\n" + value + "\n```\n"); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | public Set processFile(String name, InputStream is, String searchString) { 112 | Set keyValuePairs = null; 113 | switch (Helpers.getFileExtension(name)) { 114 | case "yaml", "yml" -> { 115 | try { 116 | keyValuePairs = Yml.process(name, is); 117 | checkConstants(name, keyValuePairs, searchString); 118 | } catch (Exception e) { 119 | logger.error("Error processing YAML file " + name, e); 120 | fallbackParsing(name, is, searchString); 121 | } 122 | } 123 | case "xml" -> { 124 | try { 125 | keyValuePairs = Xml.process(name, is, this.factory); 126 | checkConstants(name, keyValuePairs, searchString); 127 | } catch (Exception e) { 128 | logger.error("Error processing XML file " + name, e); 129 | fallbackParsing(name, is, searchString); 130 | } 131 | } 132 | case "properties" -> { 133 | try { 134 | keyValuePairs = Properties.process(name, is); 135 | checkConstants(name, keyValuePairs, searchString); 136 | } catch (Exception e) { 137 | logger.error("Error processing properties file " + name, e); 138 | fallbackParsing(name, is, searchString); 139 | } 140 | } 141 | case "zip", "jar" -> { 142 | try (ZipFile zip = Helpers.inputSteamToZipFile(is)) { 143 | List entries = (List) Collections.list(zip.entries()); 144 | 145 | entries.parallelStream() 146 | .filter(entry -> !entry.isDirectory()) 147 | .forEach(entry -> { 148 | try (InputStream zipEntryIs = zip.getInputStream(entry)) { 149 | processFile(name + "#" + entry.getName(), zipEntryIs, searchString); 150 | } catch (IOException ex) { 151 | logger.error("Error processing ZIP entry: {} in {}", entry.getName(), name, ex); 152 | } 153 | }); 154 | } catch (IOException ex) { 155 | logger.error("Error processing file " + name, ex); 156 | } 157 | } 158 | default -> { 159 | fallbackParsing(name, is, searchString); 160 | } 161 | 162 | } 163 | return keyValuePairs; 164 | } 165 | 166 | public void fallbackParsing(String name, InputStream is, String searchString) { 167 | try { 168 | String fileContent = new String(is.readAllBytes()); 169 | extractUrlsAndPaths(fileContent); 170 | 171 | if (searchString != null) { 172 | Pattern pattern = Pattern.compile(".{0,10}" + Pattern.quote(searchString) + ".{0,10}"); 173 | Matcher matcher = pattern.matcher(fileContent); 174 | 175 | while (matcher.find()) { 176 | searchResults.add("* " + name + "\n```\n" + matcher.group() + "\n```\n"); 177 | } 178 | } 179 | } catch (IOException ex) { 180 | logger.error("Error processing file in fallback " + name, ex); 181 | } 182 | } 183 | 184 | public void processConstants(String searchString) { 185 | for (JavaClass cls : jadx.getClasses()) { 186 | String fullClassName = cls.getFullName(); 187 | Set keyValuePairs = Class.process(fullClassName, cls); 188 | checkConstants(fullClassName, keyValuePairs, searchString); 189 | } 190 | for (ResourceFile resFile : jadx.getResources()) { 191 | logger.debug("Check constants in " + resFile.getDeobfName()); 192 | 193 | AtomicReference> keyValuePairs = new AtomicReference<>(); 194 | ResContainer resContainer = resFile.loadContent(); 195 | ResContainer.DataType dataType = resContainer.getDataType(); 196 | 197 | try { 198 | if (dataType == ResContainer.DataType.RES_LINK) { 199 | ResourcesLoader.decodeStream(resContainer.getResLink(), (size, is) -> { 200 | keyValuePairs.set(processFile(resFile.getDeobfName(), is, searchString)); 201 | return null; 202 | }); 203 | } 204 | 205 | if (dataType == ResContainer.DataType.TEXT) { 206 | InputStream resourceStream = Helpers.getResourceInputStream(resFile, resContainer); 207 | if (resourceStream != null) { 208 | keyValuePairs.set(processFile(resFile.getDeobfName(), resourceStream, searchString)); 209 | } 210 | } 211 | 212 | if (resFile.getType() == ResourceType.ARSC) { 213 | keyValuePairs.set(ApkResources.process(resContainer, this.factory)); 214 | } 215 | } catch (JadxException ex) { 216 | logger.error("Error processing file " + resFile.getDeobfName(), ex); 217 | } 218 | 219 | checkConstants(resFile.getDeobfName(), keyValuePairs.get(), searchString); 220 | } 221 | writerUrlsPaths.println("**Constants that look like URLs**\r\n"); 222 | urls.stream().sorted().forEach(writerUrlsPaths::println); 223 | writerUrlsPaths.flush(); 224 | writerUrlsPaths.println("\r\n\r\n**Constants that look like URI paths**\r\n"); 225 | paths.stream().sorted().forEach(writerUrlsPaths::println); 226 | writerUrlsPaths.flush(); 227 | if (writerSearch != null) { 228 | writerSearch.println("\r\n\r\n**Search results**\r\n"); 229 | searchResults.stream().sorted().forEach(writerSearch::println); 230 | writerSearch.flush(); 231 | } 232 | writerSecrets.println("**Constants that look like secrets**\r\n"); 233 | for (Map.Entry> secret : secrets.entrySet()) { 234 | for (String key : secret.getValue()) { 235 | writerSecrets.println("* " + key); 236 | } 237 | writerSecrets.println("```\n" + secret.getKey() + "\n```"); 238 | } 239 | writerSecrets.flush(); 240 | } 241 | 242 | private void extractUrlsAndPaths(String content) { 243 | try { 244 | Matcher urlMatcher = URL_PATTERN.matcher(content); 245 | while (urlMatcher.find()) { 246 | if (!urlMatcher.group().matches(EXCLUDED_LINKS_REGEXP)) { 247 | urls.add(urlMatcher.group()); 248 | } 249 | } 250 | Matcher quoteMatcher = QUOTE_PATTERN.matcher(content); 251 | while (quoteMatcher.find()) { 252 | String potentialPath = quoteMatcher.group(1); 253 | if (Helpers.isValidUrlPath(potentialPath)) { 254 | paths.add(potentialPath); 255 | } 256 | } 257 | } catch (Exception ex) { 258 | logger.error("Error in extractUrlsAndPaths", ex); 259 | } 260 | } 261 | 262 | private double calculateEntropy(String str) { 263 | int[] charCounts = new int[256]; 264 | for (char c : str.toCharArray()) { 265 | charCounts[c & 0xFF]++; 266 | } 267 | double entropy = 0.0; 268 | int len = str.length(); 269 | for (int count : charCounts) { 270 | if (count > 0) { 271 | double freq = (double) count / len; 272 | entropy -= freq * (Math.log(freq) / Math.log(2)); 273 | } 274 | } 275 | return entropy; 276 | } 277 | 278 | public boolean isLikelyText(String input) { 279 | if (input.isEmpty()) { 280 | return false; 281 | } 282 | long spaceCount = input.chars().filter(c -> c == ' ').count(); 283 | long punctuationCount = input.chars().filter(c -> ".!?,。?,".indexOf(c) >= 0).count(); 284 | double spaceRatio = (double) spaceCount / input.length(); 285 | double punctuationRatio = (double) punctuationCount / input.length(); 286 | return (spaceRatio >= 0.06) 287 | || (spaceRatio >= 0.05 && punctuationRatio >= 0.01 && punctuationRatio <= 0.1); 288 | } 289 | 290 | private boolean isBase64Like(String str) { 291 | return str.matches("^[A-Za-z0-9+/=.\r\n]{20,}$") && !str.matches("^(ru|com|org)\\..*") && (str.length() % 4 == 0); 292 | } 293 | 294 | private boolean isHexLike(String str) { 295 | return str.matches("^[A-Fa-f0-9-]{16,}$"); 296 | } 297 | 298 | private String getSecretReason(String fieldName, String value) { 299 | if (!Helpers.isPureAscii(value) || value.matches(EXCLUDED_SECRETS_REGEXP) || value.matches(EXCLUDED_LINKS_REGEXP)) { 300 | return null; 301 | } 302 | 303 | String lower = fieldName.toLowerCase(); 304 | for (String secretKeyword : SECRET_KEYWORDS) { 305 | if (lower.contains(secretKeyword) && calculateEntropy(value) >= ENTROPY_THRESHOLD_MIN && !isLikelyText(value)) { 306 | return "field name contains " + secretKeyword; 307 | } 308 | if (lower.equals(secretKeyword) || lower.endsWith(secretKeyword)) { 309 | return "field name equal " + secretKeyword; 310 | } 311 | } 312 | if (isHexLike(value)) { 313 | return "hex string"; 314 | } 315 | if (isBase64Like(value)) { 316 | return "base64-like string"; 317 | } 318 | 319 | Matcher matcher = INVALID_CONTROL_CHARS_PATTERN.matcher(value); 320 | 321 | if (calculateEntropy(value) >= ENTROPY_THRESHOLD 322 | && !isLikelyText(value) 323 | && !matcher.find()) { 324 | return "high entropy"; 325 | } 326 | return null; 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/HTTPRequestProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests; 2 | 3 | import io.swagger.v3.core.util.Yaml; 4 | import io.swagger.v3.oas.models.info.Info; 5 | import io.swagger.v3.oas.models.OpenAPI; 6 | import io.swagger.v3.oas.models.servers.Server; 7 | import io.swagger.v3.oas.models.tags.Tag; 8 | import jadx.api.JadxDecompiler; 9 | import jadx.api.JavaClass; 10 | import jadx.api.JavaMethod; 11 | import jadx.api.plugins.input.data.annotations.EncodedValue; 12 | import jadx.api.plugins.input.data.annotations.IAnnotation; 13 | import jadx.api.plugins.input.data.attributes.JadxAttrType; 14 | import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; 15 | import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; 16 | import jadx.api.plugins.input.data.IDebugInfo; 17 | import jadx.api.plugins.input.data.ILocalVar; 18 | import jadx.core.dex.instructions.args.ArgType; 19 | import jadx.core.dex.instructions.args.CodeVar; 20 | import jadx.core.dex.instructions.args.RegisterArg; 21 | import jadx.core.dex.instructions.args.SSAVar; 22 | import jadx.core.dex.nodes.MethodNode; 23 | import jadx.core.dex.nodes.RootNode; 24 | import java.io.PrintWriter; 25 | import java.net.URI; 26 | import java.util.ArrayList; 27 | import java.util.HashMap; 28 | import java.util.HashSet; 29 | import java.util.List; 30 | import java.util.Map; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | import ru.blackfan.bfscan.config.ConfigLoader; 34 | import ru.blackfan.bfscan.config.ProcessorConfig; 35 | import ru.blackfan.bfscan.parsing.httprequests.processors.AnnotationProcessor; 36 | import ru.blackfan.bfscan.parsing.httprequests.processors.AnnotationProcessorFactory; 37 | import ru.blackfan.bfscan.parsing.httprequests.processors.AnnotationUtils; 38 | import ru.blackfan.bfscan.parsing.httprequests.processors.ArgProcessingState; 39 | import ru.blackfan.bfscan.parsing.httprequests.processors.GeneralTypesConstants; 40 | import ru.blackfan.bfscan.parsing.httprequests.processors.MinifiedProcessor; 41 | 42 | public class HTTPRequestProcessor { 43 | 44 | private static final Logger logger = LoggerFactory.getLogger(HTTPRequestProcessor.class); 45 | public static final List EXCLUDED_PACKAGES = ConfigLoader.getExcludedPackages(); 46 | 47 | private final JadxDecompiler jadx; 48 | private final PrintWriter methodsWriter; 49 | private final PrintWriter openApiWriter; 50 | private final OpenAPI openApi; 51 | private final String apiBasePath; 52 | private final String apiHost; 53 | private final ResourceProcessor resourceProcessor; 54 | 55 | public HTTPRequestProcessor(JadxDecompiler jadx, URI apiUrl, boolean minifiedAnnotationsSupport, PrintWriter methodsWriter, PrintWriter openApiWriter) { 56 | this.jadx = jadx; 57 | this.apiBasePath = apiUrl.getPath(); 58 | this.apiHost = apiUrl.getHost(); 59 | this.methodsWriter = methodsWriter; 60 | this.openApiWriter = openApiWriter; 61 | this.openApi = initializeOpenApi(apiUrl); 62 | this.resourceProcessor = new ResourceProcessor(jadx, apiUrl); 63 | ProcessorConfig.getInstance().setMinifiedAnnotationsSupport(minifiedAnnotationsSupport); 64 | } 65 | 66 | private OpenAPI initializeOpenApi(URI apiUrl) { 67 | return new OpenAPI() 68 | .info(new Info().title("API Methods").version("1.0")) 69 | .addServersItem(createOpenApiServer(apiUrl)); 70 | } 71 | 72 | private Server createOpenApiServer(URI apiUrl) { 73 | String port = apiUrl.getPort() != -1 ? ":" + apiUrl.getPort() : ""; 74 | String serverUrl = String.format("%s://%s%s", apiUrl.getScheme(), apiUrl.getHost(), port); 75 | return new Server().url(serverUrl); 76 | } 77 | 78 | public void processHttpMethods() { 79 | writeHeader(); 80 | processClasses(); 81 | processResources(); 82 | writeOutput(); 83 | } 84 | 85 | private void writeHeader() { 86 | methodsWriter.println("**HTTP Methods**\r\n"); 87 | } 88 | 89 | private void buildRequests(MultiHTTPRequest request) { 90 | request.getRequests().forEach(req -> { 91 | req.addOpenApiPath(request.getClassName(), request.getMethodName(), openApi); 92 | writeRequest(req, request); 93 | }); 94 | } 95 | 96 | private void writeRequest(HTTPRequest req, MultiHTTPRequest request) { 97 | methodsWriter.println("**Method**: " + request.getClassName() + "->" + request.getMethodName() + "\r\n"); 98 | if (!req.additionalInformation.isEmpty()) { 99 | req.additionalInformation.forEach(info 100 | -> methodsWriter.println("* " + info) 101 | ); 102 | } 103 | try { 104 | methodsWriter.println("```\r\n" + req.format() + "\r\n```\r\n\r\n"); 105 | } catch (Exception ex) { 106 | logger.error("Error request format " + request.getClassName() + "->" + request.getMethodName(), ex); 107 | } 108 | methodsWriter.flush(); 109 | } 110 | 111 | private void writeOutput() { 112 | openApiWriter.print(Yaml.pretty(openApi)); 113 | openApiWriter.flush(); 114 | methodsWriter.flush(); 115 | } 116 | 117 | private void processClasses() { 118 | jadx.getClasses().stream() 119 | .filter(this::shouldProcessClass) 120 | .forEach(this::processClass); 121 | } 122 | 123 | private boolean shouldProcessClass(JavaClass cls) { 124 | return EXCLUDED_PACKAGES.stream() 125 | .noneMatch(pkg -> cls.getFullName().startsWith(pkg)); 126 | } 127 | 128 | private void processResources() { 129 | List requests = resourceProcessor.processResources(); 130 | if (requests != null) { 131 | requests.forEach(this::processRequestsFromRes); 132 | } 133 | } 134 | 135 | private void processRequestsFromRes(MultiHTTPRequest multiRequest) { 136 | addOpenApiTag(multiRequest.getClassName()); 137 | buildRequests(multiRequest); 138 | } 139 | 140 | private void processClass(JavaClass cls) { 141 | MultiHTTPRequest classRequest = new MultiHTTPRequest(apiHost, apiBasePath, cls.getFullName(), "class"); 142 | processClassAnnotations(classRequest, cls, apiBasePath); 143 | processClassMethods(cls, classRequest); 144 | } 145 | 146 | private void processClassAnnotations(MultiHTTPRequest request, JavaClass cls, String apiBasePath) { 147 | boolean buildRequest = false; 148 | AnnotationsAttr classAList = cls.getClassNode().get(JadxAttrType.ANNOTATION_LIST); 149 | if (classAList != null && !classAList.isEmpty()) { 150 | for (IAnnotation a : classAList.getAll()) { 151 | String aCls = AnnotationUtils.getAnnotationClass(a, cls.getClassNode().root()); 152 | Map annotationValues = a.getValues(); 153 | 154 | for (AnnotationProcessor processor : AnnotationProcessorFactory.getProcessors()) { 155 | if (processor.processClassAnnotations(request, aCls, annotationValues, apiBasePath, cls.getFullName(), cls.getClassNode().root())) { 156 | buildRequest = true; 157 | } 158 | } 159 | } 160 | } 161 | if (buildRequest) { 162 | addOpenApiTag(cls.getFullName()); 163 | buildRequests(request); 164 | } 165 | } 166 | 167 | public void addOpenApiTag(String className) { 168 | if (openApi.getTags() == null) { 169 | openApi.addTagsItem(new Tag().name(className)); 170 | } else if (openApi.getTags().stream().noneMatch(tag -> className.equals(tag.getName()))) { 171 | openApi.addTagsItem(new Tag().name(className)); 172 | } 173 | } 174 | 175 | private void processClassMethods(JavaClass cls, MultiHTTPRequest classRequest) { 176 | for (JavaMethod mth : cls.getMethods()) { 177 | try { 178 | processClassMethod(classRequest, cls, mth); 179 | } catch (Exception ex) { 180 | logger.error("Error processing method " + mth.getFullName(), ex); 181 | } 182 | } 183 | } 184 | 185 | private void processClassMethod(MultiHTTPRequest classRequest, JavaClass cls, JavaMethod mth) { 186 | MultiHTTPRequest methodRequest = processMethod( 187 | new MultiHTTPRequest(classRequest, cls.getFullName(), mth.getName()), 188 | mth, 189 | cls 190 | ); 191 | 192 | if (methodRequest != null) { 193 | addOpenApiTag(cls.getFullName()); 194 | buildRequests(methodRequest); 195 | } 196 | } 197 | 198 | private MultiHTTPRequest processMethod(MultiHTTPRequest request, JavaMethod mth, JavaClass cls) { 199 | RootNode rootNode = cls.getClassNode().root(); 200 | MethodNode methodNode = mth.getMethodNode(); 201 | AnnotationsAttr aList = methodNode.get(JadxAttrType.ANNOTATION_LIST); 202 | if (aList == null || aList.isEmpty()) { 203 | return null; 204 | } 205 | if (processMethodAnnotations(request, aList, rootNode)) { 206 | processMethodParameters(request, methodNode, rootNode); 207 | return request; 208 | } 209 | return null; 210 | } 211 | 212 | private boolean processMethodAnnotations(MultiHTTPRequest request, AnnotationsAttr aList, RootNode rn) { 213 | boolean buildRequest = false; 214 | for (IAnnotation annotation : aList.getAll()) { 215 | String aCls = AnnotationUtils.getAnnotationClass(annotation, rn); 216 | Map annotationValues = annotation.getValues(); 217 | 218 | if (Constants.BUILD_REQUEST_ANNOTATIONS.contains(aCls)) { 219 | buildRequest = true; 220 | } 221 | 222 | boolean processed = false; 223 | for (AnnotationProcessor processor : AnnotationProcessorFactory.getProcessors()) { 224 | try { 225 | processed |= processor.processMethodAnnotations(request, aCls, annotationValues, rn); 226 | } catch (Exception ex) { 227 | logger.error("Error in processMethodAnnotations: " + request.getClassName() + "->" + request.getMethodName()); 228 | } 229 | } 230 | if (!processed && ProcessorConfig.getInstance().isMinifiedAnnotationsSupport()) { 231 | if (MinifiedProcessor.processMethodAnnotations(request, aCls, annotationValues, rn)) { 232 | buildRequest = true; 233 | } 234 | } 235 | } 236 | return buildRequest; 237 | } 238 | 239 | private void processMethodParameters(MultiHTTPRequest request, MethodNode mn, RootNode rn) { 240 | AnnotationMethodParamsAttr paramsAnnotations = mn.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS); 241 | AnnotationUtils.loadMethodNode(mn); 242 | List localVars = getLocalVars(mn); 243 | processMethodArgs(request, mn, rn, paramsAnnotations, localVars); 244 | } 245 | 246 | private List getLocalVars(MethodNode mn) { 247 | List localVars = new ArrayList<>(); 248 | IDebugInfo debugInfo = mn.getDebugInfo(); 249 | if (debugInfo != null && debugInfo.getLocalVars() != null) { 250 | localVars = debugInfo.getLocalVars(); 251 | } 252 | return localVars; 253 | } 254 | 255 | private void processMethodArgs(MultiHTTPRequest request, MethodNode mn, RootNode rn, 256 | AnnotationMethodParamsAttr paramsAnnotations, List localVars) { 257 | List methodArgs = mn.getArgRegs(); 258 | int annNum = 0; 259 | for (RegisterArg mthArg : methodArgs) { 260 | CodeVar var = getCodeVar(mthArg); 261 | processMethodArg(request, var, paramsAnnotations, localVars, rn, mthArg.getRegNum(), annNum++); 262 | } 263 | } 264 | 265 | private CodeVar getCodeVar(RegisterArg mthArg) { 266 | SSAVar ssaVar = mthArg.getSVar(); 267 | return ssaVar == null ? CodeVar.fromMthArg(mthArg, true) : ssaVar.getCodeVar(); 268 | } 269 | 270 | private void processMethodArg(MultiHTTPRequest request, CodeVar var, 271 | AnnotationMethodParamsAttr paramsAnnotations, 272 | List localVars, RootNode rn, int regNum, int annNum) { 273 | if (paramsAnnotations != null) { 274 | processAnnotatedArg(request, var, paramsAnnotations, localVars, rn, regNum, annNum); 275 | } else { 276 | processUnannotatedArg(request, var, localVars, regNum, rn); 277 | } 278 | } 279 | 280 | private void processAnnotatedArg(MultiHTTPRequest request, CodeVar var, 281 | AnnotationMethodParamsAttr paramsAnnotations, 282 | List localVars, RootNode rootNode, int regNum, int annNum) { 283 | List paramList = paramsAnnotations.getParamList(); 284 | AnnotationsAttr argAnnList = paramList.get(annNum); 285 | 286 | if (argAnnList == null || argAnnList.isEmpty()) { 287 | processUnannotatedArg(request, var, localVars, regNum, rootNode); 288 | return; 289 | } 290 | 291 | ParameterInfo paramInfo = new ParameterInfo(); 292 | ArgProcessingState state = ArgProcessingState.NOT_PROCESSED; 293 | Map> minifiedAnnotations = new HashMap(); 294 | 295 | for (IAnnotation annotation : argAnnList.getAll()) { 296 | String aCls = AnnotationUtils.getAnnotationClass(annotation, rootNode); 297 | Map annotationValues = annotation.getValues(); 298 | 299 | ArgProcessingState currentAnnotationState = ArgProcessingState.NOT_PROCESSED; 300 | for (AnnotationProcessor processor : AnnotationProcessorFactory.getProcessors()) { 301 | try { 302 | ArgProcessingState currentProcessorState = processor.processParameterAnnotations(request, paramInfo, aCls, annotationValues, localVars, regNum, var.getType(), rootNode); 303 | if (currentProcessorState != ArgProcessingState.NOT_PROCESSED) { 304 | currentAnnotationState = currentProcessorState; 305 | if (state != ArgProcessingState.PARAMETER_CREATED) { 306 | state = currentProcessorState; 307 | } 308 | } 309 | } catch (Exception ex) { 310 | logger.error("Error in processAnnotatedArg: " + request.getClassName() + "->" + request.getMethodName()); 311 | } 312 | } 313 | if ((currentAnnotationState == ArgProcessingState.NOT_PROCESSED) && ProcessorConfig.getInstance().isMinifiedAnnotationsSupport()) { 314 | minifiedAnnotations.put(aCls, annotationValues); 315 | } 316 | } 317 | if (state != ArgProcessingState.PARAMETER_CREATED) { 318 | for (Map.Entry> entry : minifiedAnnotations.entrySet()) { 319 | ArgProcessingState currentState = MinifiedProcessor.processParameterAnnotations(request, paramInfo, entry.getKey(), entry.getValue(), localVars, regNum, var.getType(), rootNode); 320 | if (currentState == ArgProcessingState.PARAMETER_CREATED) { 321 | state = currentState; 322 | break; 323 | } 324 | } 325 | } 326 | if (state != ArgProcessingState.PARAMETER_CREATED) { 327 | processUnannotatedArg(request, var, localVars, regNum, rootNode); 328 | } 329 | } 330 | 331 | private void processUnannotatedArg(MultiHTTPRequest request, CodeVar var, List localVars, int regNum, RootNode rootNode) { 332 | ArgType argType = var.getType(); 333 | if (AnnotationUtils.isSimpleObject(argType)) { 334 | String paramName = AnnotationUtils.getParamName(null, null, localVars, regNum); 335 | AnnotationUtils.processQueryParameter(request, paramName, "", argType, rootNode); 336 | return; 337 | } else if (argType != null && argType.getObject().equals(GeneralTypesConstants.JAVA_MAP)) { 338 | if ((argType.getGenericTypes() != null) && !argType.getGenericTypes().isEmpty() && (argType.getGenericTypes().size() == 2)) { 339 | ArgType keyArg = argType.getGenericTypes().get(0); 340 | ArgType valueArg = argType.getGenericTypes().get(1); 341 | if (keyArg.getObject().equals(GeneralTypesConstants.JAVA_STRING) && valueArg.getObject().equals(GeneralTypesConstants.JAVA_STRING)) { 342 | AnnotationUtils.processQueryParameter(request, "key", "value", argType, rootNode); 343 | return; 344 | } 345 | } 346 | } 347 | if (argType != null && !argType.isPrimitive()) { 348 | try { 349 | request.putBodyParameters((Map) AnnotationUtils.argTypeToValue("value", argType, rootNode, new HashSet<>(), true)); 350 | } catch (Exception ex) { 351 | logger.error("Error in processUnannotatedArg: " + request.getClassName() + "->" + request.getMethodName()); 352 | } 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/ru/blackfan/bfscan/parsing/httprequests/processors/OpenApiProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.blackfan.bfscan.parsing.httprequests.processors; 2 | 3 | import jadx.api.plugins.input.data.annotations.EncodedValue; 4 | import jadx.api.plugins.input.data.annotations.JadxAnnotation; 5 | import jadx.api.plugins.input.data.ILocalVar; 6 | import jadx.core.dex.instructions.args.ArgType; 7 | import jadx.core.dex.nodes.ClassNode; 8 | import jadx.core.dex.nodes.RootNode; 9 | import java.net.MalformedURLException; 10 | import java.net.URI; 11 | import java.net.URISyntaxException; 12 | import java.net.URL; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Map; 16 | import ru.blackfan.bfscan.helpers.Helpers; 17 | import ru.blackfan.bfscan.parsing.httprequests.Constants; 18 | import ru.blackfan.bfscan.parsing.httprequests.MultiHTTPRequest; 19 | import ru.blackfan.bfscan.parsing.httprequests.ParameterInfo; 20 | 21 | public class OpenApiProcessor implements AnnotationProcessor { 22 | 23 | @Override 24 | public ArgProcessingState processParameterAnnotations(MultiHTTPRequest request, 25 | ParameterInfo paramInfo, 26 | String annotationClass, 27 | Map annotationValues, 28 | List localVars, 29 | int methodArg, 30 | ArgType var, 31 | RootNode rootNode) throws Exception { 32 | switch (annotationClass) { 33 | case Constants.OpenApi.PARAMETER, Constants.MicroProfileOpenApi.PARAMETER -> { 34 | processParameter(request, annotationValues, paramInfo, rootNode); 35 | return ArgProcessingState.PROCESSED_NO_PARAMETER; 36 | } 37 | default -> { 38 | return ArgProcessingState.NOT_PROCESSED; 39 | } 40 | } 41 | } 42 | 43 | @Override 44 | public boolean processMethodAnnotations(MultiHTTPRequest request, 45 | String annotationClass, 46 | Map annotationValues, 47 | RootNode rootNode) throws Exception { 48 | switch (annotationClass) { 49 | case Constants.OpenApi.PARAMETERS, Constants.MicroProfileOpenApi.PARAMETERS -> { 50 | EncodedValue value; 51 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("value"))) != null) { 52 | ArrayList parameters = (ArrayList) value.getValue(); 53 | for (EncodedValue parameter : parameters) { 54 | if (parameter.getValue() != null) { 55 | JadxAnnotation parameterAnnotation = (JadxAnnotation) parameter.getValue(); 56 | processParameter(request, parameterAnnotation.getValues(), null, rootNode); 57 | } 58 | } 59 | } 60 | return true; 61 | } 62 | case Constants.OpenApi.PARAMETER, Constants.MicroProfileOpenApi.PARAMETER -> { 63 | processParameter(request, annotationValues, null, rootNode); 64 | return true; 65 | } 66 | case Constants.OpenApi.REQUEST_BODY, Constants.MicroProfileOpenApi.REQUEST_BODY -> { 67 | processRequestBody(request, annotationValues, rootNode); 68 | return true; 69 | } 70 | case Constants.OpenApi.SERVER, Constants.MicroProfileOpenApi.SERVER -> { 71 | processServer(request, annotationValues, false, ""); 72 | return true; 73 | } 74 | case Constants.OpenApi.SERVERS, Constants.MicroProfileOpenApi.SERVERS -> { 75 | processServers(request, annotationValues, false, "", "value"); 76 | return true; 77 | } 78 | case Constants.OpenApi.OPERATION, Constants.MicroProfileOpenApi.OPERATION -> { 79 | processOperation(request, annotationValues, null, rootNode); 80 | return true; 81 | } 82 | default -> { 83 | return false; 84 | } 85 | } 86 | } 87 | 88 | @Override 89 | public boolean processClassAnnotations(MultiHTTPRequest request, 90 | String annotationClass, 91 | Map annotationValues, 92 | String globalBasePath, 93 | String className, 94 | RootNode rn 95 | ) { 96 | switch (annotationClass) { 97 | case Constants.OpenApi.SERVER, Constants.MicroProfileOpenApi.SERVER -> { 98 | processServer(request, annotationValues, true, globalBasePath); 99 | return false; 100 | } 101 | case Constants.OpenApi.SERVERS, Constants.MicroProfileOpenApi.SERVERS -> { 102 | processServers(request, annotationValues, true, globalBasePath, "value"); 103 | return false; 104 | } 105 | case Constants.OpenApi.OPENAPID_DEFINITION, Constants.MicroProfileOpenApi.OPENAPID_DEFINITION -> { 106 | processServers(request, annotationValues, true, globalBasePath, "servers"); 107 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("info")); 108 | if (value != null) { 109 | JadxAnnotation info = (JadxAnnotation) value.getValue(); 110 | processInfo(request, info.getValues()); 111 | } 112 | return false; 113 | } 114 | default -> { 115 | return false; 116 | } 117 | } 118 | } 119 | 120 | private void processInfo(MultiHTTPRequest request, 121 | Map annotationValues) { 122 | 123 | EncodedValue value; 124 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("title"))) != null) { 125 | request.addAdditionalInformation("Info Title: " + Helpers.stringWrapper(value)); 126 | } 127 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("description"))) != null) { 128 | request.addAdditionalInformation("Info Description: " + Helpers.stringWrapper(value)); 129 | } 130 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("summary"))) != null) { 131 | request.addAdditionalInformation("Info Summary: " + Helpers.stringWrapper(value)); 132 | } 133 | } 134 | 135 | private void processOperation(MultiHTTPRequest request, 136 | Map annotationValues, 137 | ParameterInfo paramInfo, 138 | RootNode rootNode) { 139 | 140 | EncodedValue value; 141 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("method"))) != null) { 142 | request.setMethod(Helpers.stringWrapper(value)); 143 | } 144 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("description"))) != null) { 145 | request.addAdditionalInformation("Operation Description: " + Helpers.stringWrapper(value)); 146 | } 147 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("summary"))) != null) { 148 | request.addAdditionalInformation("Operation Summary: " + Helpers.stringWrapper(value)); 149 | } 150 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("requestBody"))) != null) { 151 | JadxAnnotation requestBody = (JadxAnnotation) value.getValue(); 152 | processRequestBody(request, requestBody.getValues(), rootNode); 153 | } 154 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("parameters"))) != null) { 155 | ArrayList parameters = (ArrayList) value.getValue(); 156 | 157 | for (EncodedValue parameter : parameters) { 158 | if (parameter.getValue() != null) { 159 | JadxAnnotation parameterAnnotation = (JadxAnnotation) parameter.getValue(); 160 | processParameter(request, parameterAnnotation.getValues(), paramInfo, rootNode); 161 | } 162 | } 163 | } 164 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("servers"))) != null) { 165 | ArrayList servers = (ArrayList) value.getValue(); 166 | if (!servers.isEmpty()) { 167 | JadxAnnotation serverAnnotation = (JadxAnnotation) servers.get(0).getValue(); 168 | processServer(request, serverAnnotation.getValues(), false, ""); 169 | } 170 | } 171 | request.addAdditionalInformation("OpenApi Operation"); 172 | } 173 | 174 | private void processParameter(MultiHTTPRequest request, 175 | Map annotationValues, 176 | ParameterInfo paramInfo, 177 | RootNode rootNode) { 178 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("name")); 179 | if (value != null) { 180 | String parameterName = Helpers.stringWrapper(value); 181 | if (paramInfo != null) { 182 | paramInfo.setName(parameterName); 183 | } 184 | value = AnnotationUtils.getValue(annotationValues, List.of("schema")); 185 | if (value != null) { 186 | JadxAnnotation schema = (JadxAnnotation) value.getValue(); 187 | processSchema(request, schema.getValues(), paramInfo, rootNode); 188 | } 189 | value = AnnotationUtils.getValue(annotationValues, List.of("content")); 190 | if (value != null) { 191 | ArrayList contents = (ArrayList) value.getValue(); 192 | if (!contents.isEmpty()) { 193 | Map contentValues = ((JadxAnnotation) contents.get(0).getValue()).getValues(); 194 | processContent(request, contentValues, null, rootNode); 195 | } 196 | } 197 | value = AnnotationUtils.getValue(annotationValues, List.of("examples")); 198 | if (value != null) { 199 | ArrayList examples = (ArrayList) value.getValue(); 200 | if (!examples.isEmpty()) { 201 | JadxAnnotation exampleObject = (JadxAnnotation) examples.get(0).getValue(); 202 | processExampleObject(request, exampleObject.getValues(), paramInfo); 203 | } 204 | } 205 | value = AnnotationUtils.getValue(annotationValues, List.of("example")); 206 | if (value != null) { 207 | if (paramInfo != null) { 208 | paramInfo.setDefaultValue(Helpers.stringWrapper(value)); 209 | } 210 | } 211 | 212 | } 213 | } 214 | 215 | private void processRequestBody(MultiHTTPRequest request, 216 | Map annotationValues, 217 | RootNode rootNode) { 218 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("content")); 219 | if (value != null) { 220 | ArrayList contents = (ArrayList) value.getValue(); 221 | for (EncodedValue content : contents) { 222 | if (content.getValue() != null) { 223 | Map contentValues = ((JadxAnnotation) content.getValue()).getValues(); 224 | processContent(request, contentValues, null, rootNode); 225 | } 226 | } 227 | } 228 | } 229 | 230 | private void processExampleObject(MultiHTTPRequest request, 231 | Map annotationValues, 232 | ParameterInfo paramInfo) { 233 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("value")); 234 | if (value != null) { 235 | String parameterValue = Helpers.stringWrapper(value); 236 | if (paramInfo != null) { 237 | paramInfo.setDefaultValue(parameterValue); 238 | } 239 | } 240 | } 241 | 242 | private void processContent(MultiHTTPRequest request, 243 | Map annotationValues, 244 | ParameterInfo paramInfo, 245 | RootNode rootNode) { 246 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("mediaType")); 247 | if (value != null) { 248 | request.putHeader("Content-Type", Helpers.stringWrapper(value)); 249 | } 250 | if (paramInfo != null) { 251 | value = AnnotationUtils.getValue(annotationValues, List.of("examples")); 252 | if (value != null) { 253 | ArrayList examples = (ArrayList) value.getValue(); 254 | if (!examples.isEmpty()) { 255 | JadxAnnotation exampleObject = (JadxAnnotation) examples.get(0).getValue(); 256 | processExampleObject(request, exampleObject.getValues(), paramInfo); 257 | } 258 | } 259 | } 260 | value = AnnotationUtils.getValue(annotationValues, List.of("schema")); 261 | if (value != null) { 262 | JadxAnnotation schema = (JadxAnnotation) value.getValue(); 263 | processSchema(request, schema.getValues(), paramInfo, rootNode); 264 | } 265 | } 266 | 267 | private void processSchema(MultiHTTPRequest request, 268 | Map annotationValues, 269 | ParameterInfo paramInfo, 270 | RootNode rootNode) { 271 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of("name")); 272 | String parameterName = null; 273 | if (value != null) { 274 | parameterName = Helpers.stringWrapper(value); 275 | } 276 | value = AnnotationUtils.getValue(annotationValues, List.of("implementation")); 277 | if (value != null) { 278 | String className = (String) value.getValue(); 279 | ClassNode classNode = Helpers.loadClass(rootNode, className); 280 | if (classNode != null) { 281 | AnnotationUtils.processArbitraryBodyParameter(request, parameterName, classNode.getType(), rootNode); 282 | } 283 | } 284 | value = AnnotationUtils.getValue(annotationValues, List.of("exampleClasses")); 285 | 286 | if (value != null) { 287 | ArrayList exampleClasses = (ArrayList) value.getValue(); 288 | if (!exampleClasses.isEmpty()) { 289 | String className = (String) exampleClasses.get(0).getValue(); 290 | ClassNode classNode = Helpers.loadClass(rootNode, className); 291 | if (classNode != null) { 292 | AnnotationUtils.processArbitraryBodyParameter(request, parameterName, classNode.getType(), rootNode); 293 | } 294 | } 295 | } 296 | value = AnnotationUtils.getValue(annotationValues, List.of("defaultValue", "example")); 297 | if (value != null && paramInfo != null) { 298 | paramInfo.setDefaultValue(Helpers.stringWrapper(value)); 299 | } else { 300 | value = AnnotationUtils.getValue(annotationValues, List.of("allowableValues", "examples")); 301 | if (value != null && paramInfo != null) { 302 | ArrayList examples = (ArrayList) value.getValue(); 303 | if (!examples.isEmpty()) { 304 | paramInfo.setDefaultValue(Helpers.stringWrapper(examples.get(0))); 305 | } 306 | } 307 | } 308 | } 309 | 310 | private void processServers(MultiHTTPRequest request, 311 | Map annotationValues, 312 | boolean isClassAnnotation, 313 | String globalBasePath, 314 | String attrName) { 315 | EncodedValue value = AnnotationUtils.getValue(annotationValues, List.of(attrName)); 316 | if (value != null) { 317 | ArrayList servers = (ArrayList) value.getValue(); 318 | for (EncodedValue server : servers) { 319 | if (server.getValue() != null) { 320 | Map serverValues = ((JadxAnnotation) server.getValue()).getValues(); 321 | processServer(request, serverValues, isClassAnnotation, globalBasePath); 322 | } 323 | } 324 | } 325 | } 326 | 327 | private void processServer(MultiHTTPRequest request, 328 | Map annotationValues, 329 | boolean isClassAnnotation, 330 | String globalBasePath) { 331 | 332 | EncodedValue value; 333 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("url"))) != null) { 334 | String url = Helpers.stringWrapper(value); 335 | if (url.matches("https?://.*")) { 336 | try { 337 | URI absUrl = new URL(url).toURI(); 338 | if (absUrl.getPort() != -1) { 339 | request.setHost(absUrl.getHost() + ":" + absUrl.getPort()); 340 | } else { 341 | request.setHost(absUrl.getHost()); 342 | } 343 | String path = absUrl.getPath(); 344 | if ((path != null) && !path.isEmpty() && !path.equals("/")) { 345 | if (isClassAnnotation) { 346 | String classPath = path; 347 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 348 | request.setBasePaths(List.of(fullPath)); 349 | } else { 350 | request.setPath(absUrl.getPath(), false); 351 | } 352 | } 353 | } catch (MalformedURLException | URISyntaxException e) { 354 | } 355 | } else if (Helpers.isValidUrlPath(url)) { 356 | if (isClassAnnotation) { 357 | String classPath = url; 358 | String fullPath = (classPath.startsWith("/") ? globalBasePath.substring(0, globalBasePath.length() - 1) : globalBasePath) + classPath; 359 | request.setBasePaths(List.of(fullPath)); 360 | } else { 361 | request.setPath(url, false); 362 | } 363 | } else { 364 | request.addAdditionalInformation("Unrecognized Server url attribute: " + Helpers.stringWrapper(value)); 365 | } 366 | } 367 | if ((value = AnnotationUtils.getValue(annotationValues, List.of("description"))) != null) { 368 | request.addAdditionalInformation("Server Description: " + Helpers.stringWrapper(value)); 369 | } 370 | } 371 | } 372 | --------------------------------------------------------------------------------