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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ Set VM options "--module-path "...\JDKs\javafx-sdk-24.0.1\lib" --add-modules ja
# Reserved key 'VERSION' is used for users notifications only, may be skipped
VERSION=1.1

# All key names may be in 3 formats
# All key names may be in 4 formats
# KEY_NAME=VALUE - means the ferret will search for VALUE-string case-insensitive, the VALUE-string will be converted to RegExp pattern '\bVALUE\b'. Note: all spaces inside will be replaced with '\\s+', all special chars (&, -, +) will be escaped by '\\'
# KEY_NAME(regexp)=VALUE - means you have finally defined RegExp pattern, and it will be used as is
# KEY_NAME(allowed)=VALUE - means you have defined exact string - which may be found during scanned, but must be treated as allowed. Actually no matter what key name will be used - the value is a global string.
# KEY_NAME(exclude-ext)=VALUE1,VALUE2,etc.. - list of file extentions to be ignored for the "KEY_NAME" signature
# BINARY_ARTIFACTS(binary-exclude)=PATTERN1,PATTERN2,etc.. - list of regex patterns for binary files to exclude from detection (e.g., .*\.jar, .*\.png)
# Notes: all key names must be unique

Examples
Expand All @@ -69,4 +70,9 @@ PASSW-003=qwerty123
IP-ADDR(regexp)=((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
IP-ADDR-1(allowed)=0.0.0.0
IP-ADDR-2(allowed)=127.0.0.1

# Binary Artifacts Detection
# The tool automatically detects binary files (files with zero bytes in first 16KiB)
# Use binary-exclude to whitelist known binary files by their name patterns
BINARY_ARTIFACTS(binary-exclude)=.*\\.jar,.*\\.png,.*\\.jpg,.*\\.gif,.*\\.zip,.*\\.tar,.*\\.gz,.*\\.exe,.*\\.dll,.*\\.so,.*\\.dylib
```
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class RunnableScanner extends ARunnable {
private Map<String, Pattern> sigMap = null;
private Map<String, String> allowedSigMap = null;
private Map<String, List<String>> excludeExtMap = null;
private List<Pattern> binaryExcludePatterns = null;

public RunnableScanner() {
}
Expand All @@ -51,6 +52,10 @@ public void setExcludeExtMap(Map<String, List<String>> excludeExtMap) {
this.excludeExtMap = excludeExtMap;
}

public void setBinaryExcludePatterns(List<Pattern> binaryExcludePatterns) {
this.binaryExcludePatterns = binaryExcludePatterns;
}

public void setDirToScan(String dirToScan) {
this.dirToScan = dirToScan;
}
Expand Down Expand Up @@ -212,6 +217,37 @@ private void scan(FoundPathItem pathItem, Path rootDir, ExcludeFileModel exclude
if (pathItem.getType() == ItemType.DIRECTORY || pathItem.getType() == ItemType.SIGNATURE) return;

Path filePath = pathItem.getFilePath();

// Check if file is binary
boolean isBinary = false;
try {
isBinary = FileUtils.isBinaryFile(filePath);
} catch (IOException ex) {
log.warn("Could not determine if file '{}' is binary. Treating as text.", filePath);
}

// If file is binary, check if it's excluded
if (isBinary) {
boolean isExcluded = FileUtils.matchesAnyPattern(filePath, binaryExcludePatterns);
if (isExcluded) {
log.debug("Binary file '{}' is excluded from detection", filePath);
return;
}

// Flag the binary artifact
FoundPathItem binaryItem = new FoundPathItem(filePath, ItemType.SIGNATURE, pathItem);
binaryItem.setVisualName("BINARY_ARTIFACT");
binaryItem.setLineNumber(0);
binaryItem.setDisplayText("Binary file detected (contains zero bytes in first 16KiB)");
binaryItem.setFoundString(filePath.getFileName().toString());

calculateIgnoreFlagState(binaryItem, pathItem, rootDir, excludeFileModel);
foundItemsContainer.addItem(binaryItem);
log.warn("Binary artifact detected: {}", filePath);
return;
}

// For text files, proceed with normal scanning
String fileBody;
try {
fileBody = FileUtils.readFile(filePath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class RunnableSigsLoader extends ARunnable {
private Map<String, Pattern> regExpMap; // map of signatures
private Map<String, String> allowedSignaturesMap; // effectively the list of exact strings which are allowed when capturing
private Map<String, List<String>> excludeExtsMap; // signature -> List of file extensions to ignore
private List<Pattern> binaryExcludePatterns; // list of file name patterns to exclude from binary detection
private String dictionaryVersion = "undefined";

public void setFileToLoad(Path filePath) {
Expand All @@ -47,6 +48,10 @@ public Map<String, List<String>> getExcludeExtsMap() {
return excludeExtsMap;
}

public List<Pattern> getBinaryExcludePatterns() {
return binaryExcludePatterns;
}

public boolean isReady() {
return isReady.get();
}
Expand All @@ -68,6 +73,7 @@ public void _run() throws Exception {
Map<String, String> allowedSignaturesTmpMap = new HashMap<>();
Map<String, List<String>> includeExt = new HashMap<>();
Map<String, List<String>> excludeExtTmpMap = new HashMap<>();
List<Pattern> binaryExcludePatternsTmp = new ArrayList<>();

for (Object key : properties.keySet()) {
String sigId = key.toString();
Expand All @@ -93,6 +99,25 @@ public void _run() throws Exception {
continue;
}

// load binary file exclusion patterns
if (sigId.endsWith("(binary-exclude)")) {
String[] patterns = expression.split(",");
for (String patternStr : patterns) {
patternStr = patternStr.trim();
if (!patternStr.isEmpty()) {
try {
Pattern pattern = Pattern.compile(patternStr);
binaryExcludePatternsTmp.add(pattern);
log.info("Binary exclusion pattern loaded: '{}'", patternStr);
} catch (PatternSyntaxException pse) {
log.error("Error while compiling binary exclusion pattern '{}'", patternStr, pse);
}
}
}

continue;
}

if (sigId.endsWith("(allowed)")) {
sigId = sigId.substring(0, sigId.length() - 9);

Expand All @@ -106,9 +131,11 @@ public void _run() throws Exception {
regExpMap = Collections.unmodifiableMap(regExpTmpMap);
allowedSignaturesMap = Collections.unmodifiableMap(allowedSignaturesTmpMap);
excludeExtsMap = Collections.unmodifiableMap(excludeExtTmpMap);
binaryExcludePatterns = Collections.unmodifiableList(binaryExcludePatternsTmp);

log.info("Signatures are loaded successfully from {}. Number of signatures is {}", signaturesFile, regExpMap.size());
log.info("Number of allowed signatures is {}", allowedSignaturesMap.size());
log.info("Number of binary exclusion patterns is {}", binaryExcludePatterns.size());
log.info("Dictionary version is {}", dictionaryVersion);

isReady.set(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ protected TitledPane createOfflineDictionaryPane() {
runnableScanner.setSignaturesMap(runnableSigsLoader.getRegExpMap());
runnableScanner.setAllowedSigMap(runnableSigsLoader.getAllowedSignaturesMap());
runnableScanner.setExcludeExtMap(runnableSigsLoader.getExcludeExtsMap());
runnableScanner.setBinaryExcludePatterns(runnableSigsLoader.getBinaryExcludePatterns());
btnLoadSigs.setDisable(false);
});

Expand Down
49 changes: 49 additions & 0 deletions src/main/java/com/github/exadmin/cyberferret/utils/FileUtils.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.github.exadmin.cyberferret.utils;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Pattern;

public class FileUtils {
public static String readFile(String filePath) throws IOException {
Expand All @@ -30,4 +33,50 @@ public static String getFileExtensionAsString(Path path) {
? fileName.substring(dotIndex + 1)
: null;
}

/**
* Detects if a file is binary by checking if the first 16KiB contains any zero bytes.
* @param path the file path to check
* @return true if the file is detected as binary, false otherwise
* @throws IOException if an I/O error occurs
*/
public static boolean isBinaryFile(Path path) throws IOException {
if (!Files.isRegularFile(path)) {
return false;
}

// Read first 16KiB
byte[] buffer = new byte[16 * 1024];
try (InputStream is = Files.newInputStream(path)) {
int bytesRead = is.read(buffer);
if (bytesRead > 0) {
for (int i = 0; i < bytesRead; i++) {
if (buffer[i] == 0) {
return true; // Found zero byte, file is binary
}
}
}
}
return false;
}

/**
* Checks if a file name matches any of the provided patterns.
* @param path the file path to check
* @param patterns list of regex patterns to match against the file name
* @return true if the file name matches any pattern, false otherwise
*/
public static boolean matchesAnyPattern(Path path, List<Pattern> patterns) {
if (path == null || patterns == null || patterns.isEmpty()) {
return false;
}

String fileName = path.getFileName().toString();
for (Pattern pattern : patterns) {
if (pattern.matcher(fileName).matches()) {
return true;
}
}
return false;
}
}