From ebb077940b87a4976a80809b9320bedeb91cde0a Mon Sep 17 00:00:00 2001 From: lpendergrass Date: Sat, 11 Jul 2026 09:40:50 -0500 Subject: [PATCH 1/4] base unit tests --- .../embedded/YaraAreaModuleSweepTest.java | 205 +++++++++++++ .../yara/embedded/YaraAreaModuleTest.java | 278 ++++++++++++++++++ .../yara/embedded/YaraMatchEdgeCasesTest.java | 162 ++++++++++ .../resources/rules/signsrch-area-sample.yar | 100 +++++++ 4 files changed, 745 insertions(+) create mode 100644 src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleSweepTest.java create mode 100644 src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleTest.java create mode 100644 src/test/java/com/github/subreption/yara/embedded/YaraMatchEdgeCasesTest.java create mode 100644 src/test/resources/rules/signsrch-area-sample.yar diff --git a/src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleSweepTest.java b/src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleSweepTest.java new file mode 100644 index 0000000..20abb89 --- /dev/null +++ b/src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleSweepTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2024 Subreption LLC. All rights reserved. + * Copyright (c) 2015-2022 Paul Apostolescu. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.subreption.yara.embedded; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.github.subreption.yara.YaraCompilationCallback; +import com.github.subreption.yara.YaraCompiler; +import com.github.subreption.yara.YaraScanCallback; +import com.github.subreption.yara.YaraScanner; + +/** + * area module breadth and repeated-scan safety, driven by real rules from the + * /rules/signsrch-area-sample.yar fixture. Each rule's scan buffer is rebuilt from its own + * signature and scanned via scan(byte[]). + */ +public class YaraAreaModuleSweepTest { + + private YaraImpl yara; + + @BeforeEach + public void setup() { + this.yara = new YaraImpl(); + } + + @AfterEach + public void teardown() throws Exception { + yara.close(); + } + + static final class AreaRule { + final String name; + final String ruleText; + final byte[] buffer; + AreaRule(String name, String ruleText, byte[] buffer) { + this.name = name; this.ruleText = ruleText; this.buffer = buffer; + } + @Override public String toString() { return name; } + } + + private static final Pattern RULE_BLOCK = Pattern.compile("(?s)rule\\s+\\w+\\b.*?\\n\\}"); + private static final Pattern RULE_NAME = Pattern.compile("rule\\s+(\\w+)"); + private static final Pattern FIRST_HEX = Pattern.compile("\\$first\\s*=\\s*\\{\\s*([0-9A-Fa-f]+)\\s*\\}"); + private static final Pattern AREA_SCAN = + Pattern.compile("area\\.scan\\(@first,\\s*(\\d+),\\s*(\\d+),\\s*(\\d+),\\s*\"([^\"]*)\""); + private static final Pattern HEX_ESCAPE = Pattern.compile("\\\\x([0-9A-Fa-f]{2})"); + + static List loadFixtureRules() throws Exception { + URL url = YaraAreaModuleSweepTest.class.getResource("/rules/signsrch-area-sample.yar"); + assertNotNull(url, "signsrch-area-sample.yar not on the test classpath"); + String text = new String(Files.readAllBytes(Paths.get(url.toURI())), StandardCharsets.UTF_8); + + List rules = new ArrayList<>(); + Matcher blocks = RULE_BLOCK.matcher(text); + while (blocks.find()) { + String block = blocks.group(); + + Matcher nm = RULE_NAME.matcher(block); + Matcher fm = FIRST_HEX.matcher(block); + Matcher sm = AREA_SCAN.matcher(block); + assertTrue(nm.find() && fm.find() && sm.find(), "unparseable rule block:\n" + block); + + int valueSize = Integer.parseInt(sm.group(1)) / 8; + byte[] first = hexToBytes(fm.group(1)); + byte[] hexData = escapedToBytes(sm.group(4)); + + // $first + hex_data values + one value_size of padding == (value_count + 1) * value_size. + byte[] buffer = new byte[first.length + hexData.length + valueSize]; + System.arraycopy(first, 0, buffer, 0, first.length); + System.arraycopy(hexData, 0, buffer, first.length, hexData.length); + + rules.add(new AreaRule(nm.group(1), "import \"area\"\n" + block + "\n", buffer)); + } + return rules; + } + + private static byte[] hexToBytes(String hex) { + int n = hex.length() / 2; + byte[] out = new byte[n]; + for (int i = 0; i < n; i++) { + out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return out; + } + + private static byte[] escapedToBytes(String escaped) { + Matcher m = HEX_ESCAPE.matcher(escaped); + List bytes = new ArrayList<>(); + while (m.find()) { + bytes.add((byte) Integer.parseInt(m.group(1), 16)); + } + byte[] out = new byte[bytes.size()]; + for (int i = 0; i < out.length; i++) { + out[i] = bytes.get(i); + } + return out; + } + + static Stream areaRules() throws Exception { + return loadFixtureRules().stream().map(r -> Arguments.of(r)); + } + + private boolean scanForMatch(String ruleText, byte[] buffer, String expectedId) throws Exception { + YaraCompilationCallback compileCallback = + (errorLevel, fileName, lineNumber, message) -> fail("compile error [" + expectedId + "]: " + message); + final AtomicBoolean matched = new AtomicBoolean(false); + YaraScanCallback scanCallback = v -> { + if (expectedId.equals(v.getIdentifier())) { + matched.set(true); + } + }; + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback(compileCallback); + compiler.addRulesContent(ruleText, null); + try (YaraScanner scanner = compiler.createScanner()) { + scanner.setCallback(scanCallback); + scanner.scan(buffer); + } + } + return matched.get(); + } + + @Test + public void fixtureParsesAllRules() throws Exception { + assertEquals(7, loadFixtureRules().size()); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("areaRules") + public void realAreaRuleMatchesItsSignature(AreaRule rule) throws Exception { + assertTrue(scanForMatch(rule.ruleText, rule.buffer, rule.name), rule.name); + } + + // One scanner, reused across many setCallback+scan(byte[]) calls (the analyzer's block loop). + @Test + public void reusedScannerRepeatedScansDoNotCrash() throws Exception { + List rules = loadFixtureRules(); + + StringBuilder allRules = new StringBuilder("import \"area\"\n"); + for (AreaRule r : rules) { + allRules.append(r.ruleText.replaceFirst("(?s)import \"area\"\\n", "")); + } + + final int rounds = 200; + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback((errorLevel, fileName, lineNumber, message) -> fail("compile error: " + message)); + compiler.addRulesContent(allRules.toString(), null); + + try (YaraScanner scanner = compiler.createScanner()) { + for (int round = 0; round < rounds; round++) { + for (AreaRule r : rules) { + final Set matchedIds = new HashSet<>(); + scanner.setCallback(v -> matchedIds.add(v.getIdentifier())); + scanner.scan(r.buffer); + assertTrue(matchedIds.contains(r.name), r.name + " round " + round + ": " + matchedIds); + } + } + } + } + } + + @Test + public void compilerScannerLifecycleChurnDoesNotCrash() throws Exception { + AreaRule rule = loadFixtureRules().get(0); + for (int i = 0; i < 100; i++) { + assertTrue(scanForMatch(rule.ruleText, rule.buffer, rule.name), rule.name); + } + } +} diff --git a/src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleTest.java b/src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleTest.java new file mode 100644 index 0000000..5302fd3 --- /dev/null +++ b/src/test/java/com/github/subreption/yara/embedded/YaraAreaModuleTest.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2024 Subreption LLC. All rights reserved. + * Copyright (c) 2015-2022 Paul Apostolescu. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.subreption.yara.embedded; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.subreption.yara.YaraCompilationCallback; +import com.github.subreption.yara.YaraCompiler; +import com.github.subreption.yara.YaraMatch; +import com.github.subreption.yara.YaraMeta; +import com.github.subreption.yara.YaraScanCallback; +import com.github.subreption.yara.YaraScanner; + +/** + * area module tests. Rules are taken from subreption/ghidra_yara's signsrch_le_be.yar. + * Scan buffers must be at least (value_count + 1) * value_size bytes or area.scan returns 0. + */ +public class YaraAreaModuleTest { + + private static final String DES_RULE = + "import \"area\"\n" + + "rule DES : AND {\n" + + " strings:\n" + + " $first = { 0F0F0F0F }\n" + + " condition:\n" + + " $first and area.scan(@first, 32, 5, 640,\n" + + " \"\\xFF\\xFF\\x00\\x00\\x33\\x33\\x33\\x33\\xFF\\x00\\xFF\\x00\\x55\\x55\\x55\\x55\")\n" + + "}\n"; + + // $first + the four area values + one value_size of padding (24 == (5 + 1) * 4). + private static final byte[] DES_BUFFER = { + 0x0F, 0x0F, 0x0F, 0x0F, + (byte) 0xFF, (byte) 0xFF, 0x00, 0x00, + 0x33, 0x33, 0x33, 0x33, + (byte) 0xFF, 0x00, (byte) 0xFF, 0x00, + 0x55, 0x55, 0x55, 0x55, + 0x00, 0x00, 0x00, 0x00 + }; + + private YaraImpl yara; + + @BeforeEach + public void setup() { + this.yara = new YaraImpl(); + } + + @AfterEach + public void teardown() throws Exception { + yara.close(); + } + + private static File tempFileWith(byte[] data) throws Exception { + File temp = File.createTempFile(UUID.randomUUID().toString(), ".tmp"); + Files.write(Paths.get(temp.getAbsolutePath()), data, StandardOpenOption.WRITE); + return temp; + } + + private boolean scan(String rules, byte[] buffer, String expectedId) throws Exception { + File temp = tempFileWith(buffer); + YaraCompilationCallback compileCallback = + (errorLevel, fileName, lineNumber, message) -> fail("compile error: " + message); + final AtomicBoolean matched = new AtomicBoolean(false); + YaraScanCallback scanCallback = v -> { + if (expectedId.equals(v.getIdentifier())) { + matched.set(true); + } + }; + + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback(compileCallback); + compiler.addRulesContent(rules, null); + try (YaraScanner scanner = compiler.createScanner()) { + scanner.setCallback(scanCallback); + scanner.scan(temp); + } + } + return matched.get(); + } + + @Test + public void testAreaModuleIsCompiledIn() throws Exception { + String rules = "import \"area\"\nrule AreaModulePresent { condition: true }\n"; + YaraCompilationCallback compileCallback = + (errorLevel, fileName, lineNumber, message) -> fail("area import failed: " + message); + + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback(compileCallback); + compiler.addRulesContent(rules, null); + try (YaraScanner scanner = compiler.createScanner()) { + assertNotNull(scanner); + } + } + } + + @Test + public void testAreaScanDes32BitMatches() throws Exception { + assertTrue(scan(DES_RULE, DES_BUFFER, "DES")); + } + + @Test + public void testAreaScanRc564BitMatches() throws Exception { + String rules = + "import \"area\"\n" + + "rule RC5_RC5_64_P_and_RC5_64_Q : AND {\n" + + " strings:\n" + + " $first = { 6B2AED8A6251E1B7 }\n" + + " condition:\n" + + " $first and area.scan(@first, 64, 2, 512,\n" + + " \"\\x15\\x7C\\x4A\\x7F\\xB9\\x79\\x37\\x9E\")\n" + + "}\n"; + byte[] buffer = { + 0x6B, 0x2A, (byte) 0xED, (byte) 0x8A, 0x62, 0x51, (byte) 0xE1, (byte) 0xB7, + 0x15, 0x7C, 0x4A, 0x7F, (byte) 0xB9, 0x79, 0x37, (byte) 0x9E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + assertTrue(scan(rules, buffer, "RC5_RC5_64_P_and_RC5_64_Q")); + } + + @Test + public void testAreaScanDoesNotFalseMatch() throws Exception { + byte[] buffer = { // $first present, area values absent + 0x0F, 0x0F, 0x0F, 0x0F, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + }; + assertFalse(scan(DES_RULE, buffer, "DES")); + } + + @Test + public void testAreaScanBufferOneUnderGuardDoesNotMatchOrCrash() throws Exception { + byte[] buffer = { // 23 bytes, one under (5 + 1) * 4 + 0x0F, 0x0F, 0x0F, 0x0F, + (byte) 0xFF, (byte) 0xFF, 0x00, 0x00, + 0x33, 0x33, 0x33, 0x33, + (byte) 0xFF, 0x00, (byte) 0xFF, 0x00, + 0x55, 0x55, 0x55, 0x55, + 0x00, 0x00, 0x00 + }; + assertFalse(scan(DES_RULE, buffer, "DES")); + } + + @Test + public void testAreaMatchExposesCorrectRuleData() throws Exception { + String rules = + "import \"area\"\n" + + "rule AreaMatchData : AND CRYPTO {\n" + + " meta:\n" + + " description = \"DES init constants\"\n" + + " author = \"Luigi Auriemma\"\n" + + " value_count = 5\n" + + " strings:\n" + + " $first = { 0F0F0F0F }\n" + + " condition:\n" + + " $first and area.scan(@first, 32, 5, 640,\n" + + " \"\\xFF\\xFF\\x00\\x00\\x33\\x33\\x33\\x33\\xFF\\x00\\xFF\\x00\\x55\\x55\\x55\\x55\")\n" + + "}\n"; + + final AtomicBoolean verified = new AtomicBoolean(false); + YaraScanCallback scanCallback = v -> { + assertEquals("AreaMatchData", v.getIdentifier()); + + Set tags = new HashSet<>(); + v.getTags().forEachRemaining(tags::add); + assertTrue(tags.contains("AND") && tags.contains("CRYPTO")); + + Map metas = new HashMap<>(); + v.getMetadata().forEachRemaining(m -> metas.put(m.getIdentifier(), m)); + assertEquals("DES init constants", metas.get("description").getString()); + assertEquals("Luigi Auriemma", metas.get("author").getString()); + assertEquals(5, metas.get("value_count").getInteger()); + + YaraMatch m = v.getStrings().next().getMatches().next(); + assertArrayEquals(new byte[] { 0x0F, 0x0F, 0x0F, 0x0F }, m.getBytes()); + assertEquals(0L, m.getOffset()); + + verified.set(true); + }; + + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback((errorLevel, fileName, lineNumber, message) -> fail("compile error: " + message)); + compiler.addRulesContent(rules, null); + try (YaraScanner scanner = compiler.createScanner()) { + scanner.setCallback(scanCallback); + scanner.scan(DES_BUFFER); + } + } + assertTrue(verified.get()); + } + + @Test + public void testAreaScanLargeBufferReportsCorrectOffset() throws Exception { + final int matchOffset = 8_000_000; + byte[] buffer = new byte[16_000_000]; + System.arraycopy(DES_BUFFER, 0, buffer, matchOffset, DES_BUFFER.length); + + final AtomicBoolean matched = new AtomicBoolean(false); + final AtomicLong reportedOffset = new AtomicLong(-1); + YaraScanCallback scanCallback = v -> { + if ("DES".equals(v.getIdentifier())) { + reportedOffset.set(v.getStrings().next().getMatches().next().getOffset()); + matched.set(true); + } + }; + + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback((errorLevel, fileName, lineNumber, message) -> fail("compile error: " + message)); + compiler.addRulesContent(DES_RULE, null); + try (YaraScanner scanner = compiler.createScanner()) { + scanner.setCallback(scanCallback); + scanner.scan(buffer); + } + } + assertTrue(matched.get()); + assertEquals(matchOffset, reportedOffset.get()); + } + + // One scan, one rule present and one absent: guards against a build that silently never matches. + @Test + public void testAreaScanMatchesAndRejectsInOneScan() throws Exception { + String rules = + "import \"area\"\n" + + "rule DES : AND {\n" + + " strings: $first = { 0F0F0F0F }\n" + + " condition: $first and area.scan(@first, 32, 5, 640,\n" + + " \"\\xFF\\xFF\\x00\\x00\\x33\\x33\\x33\\x33\\xFF\\x00\\xFF\\x00\\x55\\x55\\x55\\x55\")\n" + + "}\n" + + "rule DecoyAbsent : AND {\n" + + " strings: $first = \"foobar\"\n" + + " condition: $first and area.scan(@first, 32, 2, 256, \"\\x11\\x22\\x33\\x44\")\n" + + "}\n"; + + Set matched = new HashSet<>(); + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback((errorLevel, fileName, lineNumber, message) -> fail("compile error: " + message)); + compiler.addRulesContent(rules, null); + try (YaraScanner scanner = compiler.createScanner()) { + scanner.setCallback(v -> matched.add(v.getIdentifier())); + scanner.scan(DES_BUFFER); + } + } + assertTrue(matched.contains("DES")); + assertFalse(matched.contains("DecoyAbsent")); + } +} diff --git a/src/test/java/com/github/subreption/yara/embedded/YaraMatchEdgeCasesTest.java b/src/test/java/com/github/subreption/yara/embedded/YaraMatchEdgeCasesTest.java new file mode 100644 index 0000000..15ac3e0 --- /dev/null +++ b/src/test/java/com/github/subreption/yara/embedded/YaraMatchEdgeCasesTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024 Subreption LLC. All rights reserved. + * Copyright (c) 2015-2022 Paul Apostolescu. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.subreption.yara.embedded; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.subreption.yara.YaraCompiler; +import com.github.subreption.yara.YaraMatch; +import com.github.subreption.yara.YaraScanner; + +/** + * Match-behavior edge cases: no-match variety, multiple matches, and match-instance iteration. + */ +public class YaraMatchEdgeCasesTest { + + private YaraImpl yara; + + @BeforeEach + public void setup() { + this.yara = new YaraImpl(); + } + + @AfterEach + public void teardown() throws Exception { + yara.close(); + } + + private static final String DES_BODY = + "rule DES : AND {\n" + + " strings:\n" + + " $first = { 0F0F0F0F }\n" + + " condition:\n" + + " $first and area.scan(@first, 32, 5, 640,\n" + + " \"\\xFF\\xFF\\x00\\x00\\x33\\x33\\x33\\x33\\xFF\\x00\\xFF\\x00\\x55\\x55\\x55\\x55\")\n" + + "}\n"; + + private static final String RC5_BODY = + "rule RC5_RC5_64_P_and_RC5_64_Q : AND {\n" + + " strings:\n" + + " $first = { 6B2AED8A6251E1B7 }\n" + + " condition:\n" + + " $first and area.scan(@first, 64, 2, 512, \"\\x15\\x7C\\x4A\\x7F\\xB9\\x79\\x37\\x9E\")\n" + + "}\n"; + + private static final byte[] DES_BLOCK = { + 0x0F, 0x0F, 0x0F, 0x0F, + (byte) 0xFF, (byte) 0xFF, 0x00, 0x00, + 0x33, 0x33, 0x33, 0x33, + (byte) 0xFF, 0x00, (byte) 0xFF, 0x00, + 0x55, 0x55, 0x55, 0x55, + 0x00, 0x00, 0x00, 0x00 + }; + private static final byte[] RC5_BLOCK = { + 0x6B, 0x2A, (byte) 0xED, (byte) 0x8A, 0x62, 0x51, (byte) 0xE1, (byte) 0xB7, + 0x15, 0x7C, 0x4A, 0x7F, (byte) 0xB9, 0x79, 0x37, (byte) 0x9E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + private static String areaRules(String... bodies) { + StringBuilder sb = new StringBuilder("import \"area\"\n"); + for (String b : bodies) { + sb.append(b); + } + return sb.toString(); + } + + private List matchedIds(String rules, byte[] buffer) throws Exception { + final List ids = new ArrayList<>(); + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback((errorLevel, fileName, lineNumber, message) -> fail("compile error: " + message)); + compiler.addRulesContent(rules, null); + try (YaraScanner scanner = compiler.createScanner()) { + scanner.setCallback(v -> ids.add(v.getIdentifier())); + scanner.scan(buffer); + } + } + return ids; + } + + @Test + public void noMatchWhenPatternAbsent() throws Exception { + assertTrue(matchedIds(areaRules(DES_BODY), new byte[64]).isEmpty()); + } + + @Test + public void noMatchWhenAreaValuesOutOfRange() throws Exception { + byte[] buffer = new byte[6000]; + System.arraycopy(DES_BLOCK, 0, buffer, 0, 4); // $first at offset 0 + System.arraycopy(DES_BLOCK, 4, buffer, 5000, 16); // values well past scan_range (640) + assertTrue(matchedIds(areaRules(DES_BODY), buffer).isEmpty()); + } + + @Test + public void multipleDistinctRulesMatchOneBuffer() throws Exception { + byte[] buffer = new byte[200]; + System.arraycopy(DES_BLOCK, 0, buffer, 0, DES_BLOCK.length); + System.arraycopy(RC5_BLOCK, 0, buffer, 100, RC5_BLOCK.length); + + List ids = matchedIds(areaRules(DES_BODY, RC5_BODY), buffer); + assertTrue(ids.contains("DES"), "got " + ids); + assertTrue(ids.contains("RC5_RC5_64_P_and_RC5_64_Q"), "got " + ids); + } + + @Test + public void stringMatchInstancesIteratedExactly() throws Exception { + final int n = 64; + final int stride = 8; // 4-byte pattern + 4-byte gap, non-overlapping + final byte[] pattern = { 'M', 'A', 'R', 'K' }; + + byte[] buffer = new byte[n * stride]; + Set expectedOffsets = new HashSet<>(); + for (int i = 0; i < n; i++) { + System.arraycopy(pattern, 0, buffer, i * stride, pattern.length); + expectedOffsets.add((long) (i * stride)); + } + String rule = "rule Marks { strings: $a = \"MARK\" condition: #a == " + n + " }"; + + final Set seenOffsets = new HashSet<>(); + final AtomicBoolean fired = new AtomicBoolean(false); + try (YaraCompiler compiler = yara.createCompiler()) { + compiler.setCallback((errorLevel, fileName, lineNumber, message) -> fail("compile error: " + message)); + compiler.addRulesContent(rule, null); + try (YaraScanner scanner = compiler.createScanner()) { + scanner.setCallback(v -> { + fired.set(true); + for (Iterator it = v.getStrings().next().getMatches(); it.hasNext(); ) { + seenOffsets.add(it.next().getOffset()); + } + }); + scanner.scan(buffer); + } + } + assertTrue(fired.get()); + assertEquals(expectedOffsets, seenOffsets); + } +} diff --git a/src/test/resources/rules/signsrch-area-sample.yar b/src/test/resources/rules/signsrch-area-sample.yar new file mode 100644 index 0000000..da8b28f --- /dev/null +++ b/src/test/resources/rules/signsrch-area-sample.yar @@ -0,0 +1,100 @@ +/* + * Curated sample of REAL area.scan rules, extracted verbatim from + * subreption/ghidra_yara's data/rules/signsrch/signsrch_le_be.yar + * (Luigi Auriemma's signsrch.sig, converted by Kevin Weatherman). + * Self-contained test fixture spanning value_bits 32/64 and value_count 2..88. + * Consumed by YaraAreaModuleSweepTest. Do not hand-edit. + */ +import "area" + +rule TEA_encryption_decryption_0xc6ef3720_0x9e3779b9 : AND +{ + meta: + description = "TEA encryption/decryption (0xc6ef3720 0x9e3779b9) [32.le.8&]" + author = "Luigi Auriemma" + strings: + $first = { 2037EFC6} // [0] 0xC6EF3720 + condition: + $first and area.scan(@first, 32, 2, 256, + "\xB9\x79\x37\x9E") + /* [1] 0x9E3779B9 */ +} + +rule MD4_digest : AND +{ + meta: + description = "MD4 digest [32.le.24&]" + author = "Luigi Auriemma" + strings: + $first = { 01234567} // [0] 0x67452301 + condition: + $first and area.scan(@first, 32, 6, 768, + "\x89\xAB\xCD\xEF\xFE\xDC\xBA\x98\x76\x54\x32\x10\x99\x79\x82\x5A\xA1\xEB\xD9\x6E") + /* [1] 0xEFCDAB89, [2] 0x98BADCFE, [3] 0x10325476, [4] 0x5A827999, [5] 0x6ED9EBA1 */ +} + +rule SSL2_define : AND +{ + meta: + description = "SSL2 #define [32.le.52&]" + author = "Luigi Auriemma" + strings: + $first = { 00000002} // [0] 0x2000000 + condition: + $first and area.scan(@first, 32, 13, 1664, + "\x80\x00\x01\x02\x80\x00\x02\x02\x80\x00\x03\x02\x80\x00\x04\x02\x80\x00\x05\x02\x40\x00\x06\x02\x40\x01\x06\x02\xC0\x00\x07\x02\xC0\x01\x07\x02\x80\x00\x08\x02\x00\x08\xFF\x02\x10\x08\xFF\x02") + /* [1] 0x02010080, [2] 0x02020080, [3] 0x02030080, [4] 0x02040080, [5] 0x02050080, [6] 0x02060040, [7] 0x02060140, [8] 0x020700C0, [9] 0x020701C0, [10] 0x02080080, [11] 0x02FF0800, [12] 0x02FF0810 */ +} + +rule SHA224 : AND +{ + meta: + description = "SHA224 [32.le.288&]" + author = "Luigi Auriemma" + strings: + $first = { D89E05C1} // [0] 0xC1059ED8 + condition: + $first and area.scan(@first, 32, 72, 3000, + "\x07\xD5\x7C\x36\x17\xDD\x70\x30\x39\x59\x0E\xF7\x31\x0B\xC0\xFF\x11\x15\x58\x68\xA7\x8F\xF9\x64\xA4\x4F\xFA\xBE\x98\x2F\x8A\x42\x91\x44\x37\x71\xCF\xFB\xC0\xB5\xA5\xDB\xB5\xE9\x5B\xC2\x56\x39\xF1\x11\xF1\x59\xA4\x82\x3F\x92\xD5\x5E\x1C\xAB\x98\xAA\x07\xD8\x01\x5B\x83\x12\xBE\x85\x31\x24\xC3\x7D\x0C\x55\x74\x5D\xBE\x72\xFE\xB1\xDE\x80\xA7\x06\xDC\x9B\x74\xF1\x9B\xC1\xC1\x69\x9B\xE4\x86\x47\xBE\xEF\xC6\x9D\xC1\x0F\xCC\xA1\x0C\x24\x6F\x2C\xE9\x2D\xAA\x84\x74\x4A\xDC\xA9\xB0\x5C\xDA\x88\xF9\x76\x52\x51\x3E\x98\x6D\xC6\x31\xA8\xC8\x27\x03\xB0\xC7\x7F\x59\xBF\xF3\x0B\xE0\xC6\x47\x91\xA7\xD5\x51\x63\xCA\x06\x67\x29\x29\x14\x85\x0A\xB7\x27\x38\x21\x1B\x2E\xFC\x6D\x2C\x4D\x13\x0D\x38\x53\x54\x73\x0A\x65\xBB\x0A\x6A\x76\x2E\xC9\xC2\x81\x85\x2C\x72\x92\xA1\xE8\xBF\xA2\x4B\x66\x1A\xA8\x70\x8B\x4B\xC2\xA3\x51\x6C\xC7\x19\xE8\x92\xD1\x24\x06\x99\xD6\x85\x35\x0E\xF4\x70\xA0\x6A\x10\x16\xC1\xA4\x19\x08\x6C\x37\x1E\x4C\x77\x48\x27\xB5\xBC\xB0\x34\xB3\x0C\x1C\x39\x4A\xAA\xD8\x4E\x4F\xCA\x9C\x5B\xF3\x6F\x2E\x68\xEE\x82\x8F\x74\x6F\x63\xA5\x78\x14\x78\xC8\x84\x08\x02\xC7\x8C\xFA\xFF\xBE\x90\xEB\x6C\x50\xA4\xF7\xA3\xF9\xBE\xF2\x78\x71\xC6") + /* [1] 0x367CD507, [2] 0x3070DD17, [3] 0xF70E5939, [4] 0xFFC00B31, [5] 0x68581511, [6] 0x64F98FA7, [7] 0xBEFA4FA4, [8] 0x428A2F98, [9] 0x71374491, [10] 0xB5C0FBCF, [11] 0xE9B5DBA5, [12] 0x3956C25B, [13] 0x59F111F1, [14] 0x923F82A4, [15] 0xAB1C5ED5, [16] 0xD807AA98, [17] 0x12835B01, [18] 0x243185BE, [19] 0x550C7DC3, [20] 0x72BE5D74, [21] 0x80DEB1FE, [22] 0x9BDC06A7, [23] 0xC19BF174, [24] 0xE49B69C1, [25] 0xEFBE4786, [26] 0x0FC19DC6, [27] 0x240CA1CC, [28] 0x2DE92C6F, [29] 0x4A7484AA, [30] 0x5CB0A9DC, [31] 0x76F988DA, [32] 0x983E5152, [33] 0xA831C66D, [34] 0xB00327C8, [35] 0xBF597FC7, [36] 0xC6E00BF3, [37] 0xD5A79147, [38] 0x06CA6351, [39] 0x14292967, [40] 0x27B70A85, [41] 0x2E1B2138, [42] 0x4D2C6DFC, [43] 0x53380D13, [44] 0x650A7354, [45] 0x766A0ABB, [46] 0x81C2C92E, [47] 0x92722C85, [48] 0xA2BFE8A1, [49] 0xA81A664B, [50] 0xC24B8B70, [51] 0xC76C51A3, [52] 0xD192E819, [53] 0xD6990624, [54] 0xF40E3585, [55] 0x106AA070, [56] 0x19A4C116, [57] 0x1E376C08, [58] 0x2748774C, [59] 0x34B0BCB5, [60] 0x391C0CB3, [61] 0x4ED8AA4A, [62] 0x5B9CCA4F, [63] 0x682E6FF3, [64] 0x748F82EE, [65] 0x78A5636F, [66] 0x84C87814, [67] 0x8CC70208, [68] 0x90BEFFFA, [69] 0xA4506CEB, [70] 0xBEF9A3F7, [71] 0xC67178F2 */ +} + +rule RC5_RC5_64_P_and_RC5_64_Q : AND +{ + meta: + description = "RC5 RC5_64_P and RC5_64_Q [64.le.16&]" + author = "Luigi Auriemma" + strings: + $first = { 6B2AED8A6251E1B7} // [0] 0xB7E151628AED2A6B + condition: + $first and area.scan(@first, 64, 2, 512, + "\x15\x7C\x4A\x7F\xB9\x79\x37\x9E") + /* [1] 0x9E3779B97F4A7C15 */ +} + +rule Initial_hash_value_H_for_SHA_384_and_SHA_512 : AND +{ + meta: + description = "Initial hash value H for SHA-384 and SHA-512 [64.le.64&]" + author = "Luigi Auriemma" + strings: + $first = { D89E05C15D9DBBCB} // [0] 0xCBBB9D5DC1059ED8 + condition: + $first and area.scan(@first, 64, 8, 2048, + "\x07\xD5\x7C\x36\x2A\x29\x9A\x62\x17\xDD\x70\x30\x5A\x01\x59\x91\x39\x59\x0E\xF7\xD8\xEC\x2F\x15\x31\x0B\xC0\xFF\x67\x26\x33\x67\x11\x15\x58\x68\x87\x4A\xB4\x8E\xA7\x8F\xF9\x64\x0D\x2E\x0C\xDB\xA4\x4F\xFA\xBE\x1D\x48\xB5\x47") + /* [1] 0x629A292A367CD507, [2] 0x9159015A3070DD17, [3] 0x152FECD8F70E5939, [4] 0x67332667FFC00B31, [5] 0x8EB44A8768581511, [6] 0xDB0C2E0D64F98FA7, [7] 0x47B5481DBEFA4FA4 */ +} + +rule SHA384 : AND +{ + meta: + description = "SHA384 [64.le.704&]" + author = "Luigi Auriemma" + strings: + $first = { D89E05C15D9DBBCB} // [0] 0xCBBB9D5DC1059ED8 + condition: + $first and area.scan(@first, 64, 88, 3000, + "\x07\xD5\x7C\x36\x2A\x29\x9A\x62\x17\xDD\x70\x30\x5A\x01\x59\x91\x39\x59\x0E\xF7\xD8\xEC\x2F\x15\x31\x0B\xC0\xFF\x67\x26\x33\x67\x11\x15\x58\x68\x87\x4A\xB4\x8E\xA7\x8F\xF9\x64\x0D\x2E\x0C\xDB\xA4\x4F\xFA\xBE\x1D\x48\xB5\x47\x22\xAE\x28\xD7\x98\x2F\x8A\x42\xCD\x65\xEF\x23\x91\x44\x37\x71\x2F\x3B\x4D\xEC\xCF\xFB\xC0\xB5\xBC\xDB\x89\x81\xA5\xDB\xB5\xE9\x38\xB5\x48\xF3\x5B\xC2\x56\x39\x19\xD0\x05\xB6\xF1\x11\xF1\x59\x9B\x4F\x19\xAF\xA4\x82\x3F\x92\x18\x81\x6D\xDA\xD5\x5E\x1C\xAB\x42\x02\x03\xA3\x98\xAA\x07\xD8\xBE\x6F\x70\x45\x01\x5B\x83\x12\x8C\xB2\xE4\x4E\xBE\x85\x31\x24\xE2\xB4\xFF\xD5\xC3\x7D\x0C\x55\x6F\x89\x7B\xF2\x74\x5D\xBE\x72\xB1\x96\x16\x3B\xFE\xB1\xDE\x80\x35\x12\xC7\x25\xA7\x06\xDC\x9B\x94\x26\x69\xCF\x74\xF1\x9B\xC1\xD2\x4A\xF1\x9E\xC1\x69\x9B\xE4\xE3\x25\x4F\x38\x86\x47\xBE\xEF\xB5\xD5\x8C\x8B\xC6\x9D\xC1\x0F\x65\x9C\xAC\x77\xCC\xA1\x0C\x24\x75\x02\x2B\x59\x6F\x2C\xE9\x2D\x83\xE4\xA6\x6E\xAA\x84\x74\x4A\xD4\xFB\x41\xBD\xDC\xA9\xB0\x5C\xB5\x53\x11\x83\xDA\x88\xF9\x76\xAB\xDF\x66\xEE\x52\x51\x3E\x98\x10\x32\xB4\x2D\x6D\xC6\x31\xA8\x3F\x21\xFB\x98\xC8\x27\x03\xB0\xE4\x0E\xEF\xBE\xC7\x7F\x59\xBF\xC2\x8F\xA8\x3D\xF3\x0B\xE0\xC6\x25\xA7\x0A\x93\x47\x91\xA7\xD5\x6F\x82\x03\xE0\x51\x63\xCA\x06\x70\x6E\x0E\x0A\x67\x29\x29\x14\xFC\x2F\xD2\x46\x85\x0A\xB7\x27\x26\xC9\x26\x5C\x38\x21\x1B\x2E\xED\x2A\xC4\x5A\xFC\x6D\x2C\x4D\xDF\xB3\x95\x9D\x13\x0D\x38\x53\xDE\x63\xAF\x8B\x54\x73\x0A\x65\xA8\xB2\x77\x3C\xBB\x0A\x6A\x76\xE6\xAE\xED\x47\x2E\xC9\xC2\x81\x3B\x35\x82\x14\x85\x2C\x72\x92\x64\x03\xF1\x4C\xA1\xE8\xBF\xA2\x01\x30\x42\xBC\x4B\x66\x1A\xA8\x91\x97\xF8\xD0\x70\x8B\x4B\xC2\x30\xBE\x54\x06\xA3\x51\x6C\xC7\x18\x52\xEF\xD6\x19\xE8\x92\xD1\x10\xA9\x65\x55\x24\x06\x99\xD6\x2A\x20\x71\x57\x85\x35\x0E\xF4\xB8\xD1\xBB\x32\x70\xA0\x6A\x10\xC8\xD0\xD2\xB8\x16\xC1\xA4\x19\x53\xAB\x41\x51\x08\x6C\x37\x1E\x99\xEB\x8E\xDF\x4C\x77\x48\x27\xA8\x48\x9B\xE1\xB5\xBC\xB0\x34\x63\x5A\xC9\xC5\xB3\x0C\x1C\x39\xCB\x8A\x41\xE3\x4A\xAA\xD8\x4E\x73\xE3\x63\x77\x4F\xCA\x9C\x5B\xA3\xB8\xB2\xD6\xF3\x6F\x2E\x68\xFC\xB2\xEF\x5D\xEE\x82\x8F\x74\x60\x2F\x17\x43\x6F\x63\xA5\x78\x72\xAB\xF0\xA1\x14\x78\xC8\x84\xEC\x39\x64\x1A\x08\x02\xC7\x8C\x28\x1E\x63\x23\xFA\xFF\xBE\x90\xE9\xBD\x82\xDE\xEB\x6C\x50\xA4\x15\x79\xC6\xB2\xF7\xA3\xF9\xBE\x2B\x53\x72\xE3\xF2\x78\x71\xC6\x9C\x61\x26\xEA\xCE\x3E\x27\xCA\x07\xC2\xC0\x21\xC7\xB8\x86\xD1\x1E\xEB\xE0\xCD\xD6\x7D\xDA\xEA\x78\xD1\x6E\xEE\x7F\x4F\x7D\xF5\xBA\x6F\x17\x72\xAA\x67\xF0\x06\xA6\x98\xC8\xA2\xC5\x7D\x63\x0A\xAE\x0D\xF9\xBE\x04\x98\x3F\x11\x1B\x47\x1C\x13\x35\x0B\x71\x1B\x84\x7D\x04\x23\xF5\x77\xDB\x28\x93\x24\xC7\x40\x7B\xAB\xCA\x32\xBC\xBE\xC9\x15\x0A\xBE\x9E\x3C\x4C\x0D\x10\x9C\xC4\x67\x1D\x43\xB6\x42\x3E\xCB\xBE\xD4\xC5\x4C\x2A\x7E\x65\xFC\x9C\x29\x7F\x59\xEC\xFA\xD6\x3A\xAB\x6F\xCB\x5F\x17\x58\x47\x4A\x8C\x19\x44\x6C") + /* [1] 0x629A292A367CD507, [2] 0x9159015A3070DD17, [3] 0x152FECD8F70E5939, [4] 0x67332667FFC00B31, [5] 0x8EB44A8768581511, [6] 0xDB0C2E0D64F98FA7, [7] 0x47B5481DBEFA4FA4, [8] 0x428A2F98D728AE22, [9] 0x7137449123EF65CD, [10] 0xB5C0FBCFEC4D3B2F, [11] 0xE9B5DBA58189DBBC, [12] 0x3956C25BF348B538, [13] 0x59F111F1B605D019, [14] 0x923F82A4AF194F9B, [15] 0xAB1C5ED5DA6D8118, [16] 0xD807AA98A3030242, [17] 0x12835B0145706FBE, [18] 0x243185BE4EE4B28C, [19] 0x550C7DC3D5FFB4E2, [20] 0x72BE5D74F27B896F, [21] 0x80DEB1FE3B1696B1, [22] 0x9BDC06A725C71235, [23] 0xC19BF174CF692694, [24] 0xE49B69C19EF14AD2, [25] 0xEFBE4786384F25E3, [26] 0x0FC19DC68B8CD5B5, [27] 0x240CA1CC77AC9C65, [28] 0x2DE92C6F592B0275, [29] 0x4A7484AA6EA6E483, [30] 0x5CB0A9DCBD41FBD4, [31] 0x76F988DA831153B5, [32] 0x983E5152EE66DFAB, [33] 0xA831C66D2DB43210, [34] 0xB00327C898FB213F, [35] 0xBF597FC7BEEF0EE4, [36] 0xC6E00BF33DA88FC2, [37] 0xD5A79147930AA725, [38] 0x06CA6351E003826F, [39] 0x142929670A0E6E70, [40] 0x27B70A8546D22FFC, [41] 0x2E1B21385C26C926, [42] 0x4D2C6DFC5AC42AED, [43] 0x53380D139D95B3DF, [44] 0x650A73548BAF63DE, [45] 0x766A0ABB3C77B2A8, [46] 0x81C2C92E47EDAEE6, [47] 0x92722C851482353B, [48] 0xA2BFE8A14CF10364, [49] 0xA81A664BBC423001, [50] 0xC24B8B70D0F89791, [51] 0xC76C51A30654BE30, [52] 0xD192E819D6EF5218, [53] 0xD69906245565A910, [54] 0xF40E35855771202A, [55] 0x106AA07032BBD1B8, [56] 0x19A4C116B8D2D0C8, [57] 0x1E376C085141AB53, [58] 0x2748774CDF8EEB99, [59] 0x34B0BCB5E19B48A8, [60] 0x391C0CB3C5C95A63, [61] 0x4ED8AA4AE3418ACB, [62] 0x5B9CCA4F7763E373, [63] 0x682E6FF3D6B2B8A3, [64] 0x748F82EE5DEFB2FC, [65] 0x78A5636F43172F60, [66] 0x84C87814A1F0AB72, [67] 0x8CC702081A6439EC, [68] 0x90BEFFFA23631E28, [69] 0xA4506CEBDE82BDE9, [70] 0xBEF9A3F7B2C67915, [71] 0xC67178F2E372532B, [72] 0xCA273ECEEA26619C, [73] 0xD186B8C721C0C207, [74] 0xEADA7DD6CDE0EB1E, [75] 0xF57D4F7FEE6ED178, [76] 0x06F067AA72176FBA, [77] 0x0A637DC5A2C898A6, [78] 0x113F9804BEF90DAE, [79] 0x1B710B35131C471B, [80] 0x28DB77F523047D84, [81] 0x32CAAB7B40C72493, [82] 0x3C9EBE0A15C9BEBC, [83] 0x431D67C49C100D4C, [84] 0x4CC5D4BECB3E42B6, [85] 0x597F299CFC657E2A, [86] 0x5FCB6FAB3AD6FAEC, [87] 0x6C44198C4A475817 */ +} + From 5dd470bcf84964a397d084b047dddfe0f94c2403 Mon Sep 17 00:00:00 2001 From: lpendergrass Date: Sat, 11 Jul 2026 19:26:17 -0500 Subject: [PATCH 2/4] Windows MSVC build --- pom.xml | 70 +++++++++++++++++++++ src/main/native-package/vs2010.custom.props | 15 +++++ 2 files changed, 85 insertions(+) create mode 100644 src/main/native-package/vs2010.custom.props diff --git a/pom.xml b/pom.xml index 3b9cbb7..9234bb0 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,7 @@ ${hawtjni-version} + build-yara-wrapper compile generate @@ -282,5 +283,74 @@ + + windows-amd64 + + + windows + amd64 + + + + + + + org.fusesource.hawtjni + hawtjni-maven-plugin + ${hawtjni-version} + + + build-yara-wrapper + + true + msbuild + v143 + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + test-native + + + + + + + + package-native + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + **/external/** + + + + + + diff --git a/src/main/native-package/vs2010.custom.props b/src/main/native-package/vs2010.custom.props new file mode 100644 index 0000000..89e936a --- /dev/null +++ b/src/main/native-package/vs2010.custom.props @@ -0,0 +1,15 @@ + + + + + + $(YARA_HOME)\libyara\include;%(AdditionalIncludeDirectories) + + JNI64;%(PreprocessorDefinitions) + + + $(YARA_HOME)\windows\vs2017\libyara\Release\libyara64.lib;%(AdditionalDependencies) + + + From 8a860486e30433f2a348eba76df5cfa628328971 Mon Sep 17 00:00:00 2001 From: lpendergrass Date: Sat, 11 Jul 2026 19:28:13 -0500 Subject: [PATCH 3/4] Windows CI support --- .github/workflows/build_multi.yml | 72 +++++++++++++++++++++- .github/workflows/build_windows64.yml | 71 +++++++++++++++++++++ .github/workflows/ci_nightly.yml | 88 ++++++++++++++++++++++++++- .github/workflows/ci_release.yml | 88 ++++++++++++++++++++++++++- 4 files changed, 313 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/build_windows64.yml diff --git a/.github/workflows/build_multi.yml b/.github/workflows/build_multi.yml index 607df76..e899183 100644 --- a/.github/workflows/build_multi.yml +++ b/.github/workflows/build_multi.yml @@ -132,8 +132,69 @@ jobs: path: target/*-osx64.jar overwrite: true + build_windows64: + runs-on: windows-latest + + strategy: + matrix: + jdk: [21] + libyara_tag: ["v4.5.2-subreption"] + arch: [windows64] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: ${{ matrix.jdk }} + settings-path: ${{ github.workspace }} + + - name: Set up MSVC developer environment (x64) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Set up NuGet + uses: NuGet/setup-nuget@v2 + + - name: Clone libyara (${{ matrix.libyara_tag }}) + uses: actions/checkout@v4 + with: + repository: subreption/yara + path: yara + ref: ${{ matrix.libyara_tag }} + token: ${{ secrets.GH_YARA_REPO_PAT }} + + # packages.config is not auto-restored by MSBuild. + - name: Restore libyara NuGet packages + run: nuget restore yara/windows/vs2017/yara.sln + + # libyara only (JNI embedded; no yara/yarac). + - name: Build libyara (MSVC Release|x64) + run: | + msbuild "yara/windows/vs2017/libyara/libyara.vcxproj" /p:Configuration=Release /p:Platform=x64 /v:minimal + if ($LASTEXITCODE -ne 0) { throw "msbuild failed for libyara" } + + - name: Export YARA_HOME + run: echo "YARA_HOME=${{ github.workspace }}\yara" >> $env:GITHUB_ENV + + - name: Build YARA-Java (Windows) + run: mvn -B package --file pom.xml + env: + YARA_HOME: ${{ env.YARA_HOME }} + + - name: Upload windows64 build artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.arch }}-artifacts + path: target/*-windows64.jar + overwrite: true + bundle: - needs: [build_linux, build_mac] + needs: [build_linux, build_mac, build_windows64] runs-on: ubuntu-latest steps: @@ -149,15 +210,22 @@ jobs: name: osx64-artifacts path: build/osx64 + - name: Download windows64 build artifacts + uses: actions/download-artifact@v4 + with: + name: windows64-artifacts + path: build/windows64 + - name: List current working directory run: ls -lhRt - # Step to bundle Linux and macOS JARs + # Step to bundle Linux, macOS, and Windows JARs - name: Bundle multiplatform JARs run: | mkdir -p bundled-jars cp build/linux64/*.jar bundled-jars/ cp build/osx64/*.jar bundled-jars/ + cp build/windows64/*.jar bundled-jars/ - name: Upload bundled JARs uses: actions/upload-artifact@v4 diff --git a/.github/workflows/build_windows64.yml b/.github/workflows/build_windows64.yml new file mode 100644 index 0000000..95061f0 --- /dev/null +++ b/.github/workflows/build_windows64.yml @@ -0,0 +1,71 @@ +name: Build (windows64) + +on: + workflow_dispatch: + pull_request: + push: + +jobs: + + build_windows64: + runs-on: windows-latest + + strategy: + matrix: + jdk: [21] + libyara_tag: ["v4.5.2-subreption"] + arch: [windows64] + + steps: + # Step 1: Checkout the repository + - name: Checkout code + uses: actions/checkout@v4 + + # Step 2: Set up JDK + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: ${{ matrix.jdk }} + + # Step 3: MSVC dev environment (exported to all later steps). + - name: Set up MSVC developer environment (x64) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Set up NuGet + uses: NuGet/setup-nuget@v2 + + # Step 4: Clone libyara. + - name: Clone libyara (${{ matrix.libyara_tag }}) + uses: actions/checkout@v4 + with: + repository: subreption/yara + path: yara + ref: ${{ matrix.libyara_tag }} + token: ${{ secrets.GH_YARA_REPO_PAT }} + + # Step 5: Restore NuGet deps (packages.config is not auto-restored). + - name: Restore libyara NuGet packages + run: nuget restore yara/windows/vs2017/yara.sln + + # Step 6: Build libyara only (JNI embedded; no yara/yarac). + - name: Build libyara (MSVC Release|x64) + run: | + msbuild "yara/windows/vs2017/libyara/libyara.vcxproj" /p:Configuration=Release /p:Platform=x64 /v:minimal + if ($LASTEXITCODE -ne 0) { throw "msbuild failed for libyara" } + + - name: Export YARA_HOME + run: echo "YARA_HOME=${{ github.workspace }}\yara" >> $env:GITHUB_ENV + + # Step 7: Build + test yara-java (windows-amd64 profile auto-activates). + - name: Build and test yara-java (mvn package) + run: mvn -B package --file pom.xml + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.arch }}-artifacts + path: target/*.jar + if-no-files-found: error diff --git a/.github/workflows/ci_nightly.yml b/.github/workflows/ci_nightly.yml index f53e259..c2493e9 100644 --- a/.github/workflows/ci_nightly.yml +++ b/.github/workflows/ci_nightly.yml @@ -173,8 +173,85 @@ jobs: outputs: mac-artifact: ${{ steps.build.outputs.maven-artifact }} + build_windows64: + runs-on: windows-latest + + strategy: + matrix: + jdk: [21] + libyara_tag: [ "v4.5.2-subreption" ] + arch: [windows64] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: ${{ matrix.jdk }} + settings-path: ${{ github.workspace }} + + - name: Set up MSVC developer environment (x64) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Set up NuGet + uses: NuGet/setup-nuget@v2 + + - name: Clone libyara (${{ matrix.libyara_tag }}) + uses: actions/checkout@v4 + with: + repository: subreption/yara + path: yara + ref: ${{ matrix.libyara_tag }} + token: ${{ secrets.GH_YARA_REPO_PAT }} + + # packages.config is not auto-restored by MSBuild. + - name: Restore libyara NuGet packages + run: nuget restore yara/windows/vs2017/yara.sln + + # libyara only (JNI embedded; no yara/yarac). + - name: Build libyara (MSVC Release|x64) + run: | + msbuild "yara/windows/vs2017/libyara/libyara.vcxproj" /p:Configuration=Release /p:Platform=x64 /v:minimal + if ($LASTEXITCODE -ne 0) { throw "msbuild failed for libyara" } + + - name: Export YARA_HOME + run: echo "YARA_HOME=${{ github.workspace }}\yara" >> $env:GITHUB_ENV + + - name: Build YARA-Java (Windows) + run: mvn -B package --file pom.xml + env: + YARA_HOME: ${{ env.YARA_HOME }} + + - name: Upload windows64 build artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.arch }}-artifacts + path: target/*-windows64.jar + overwrite: true + + - name: Set up Maven settings.xml for GitHub Packages + shell: bash + run: | + mkdir -p ~/.m2 + echo "github${{ github.actor }}${{ secrets.GITHUB_TOKEN }}" > ~/.m2/settings.xml + + - name: Deploy Maven artifacts + shell: bash + run: mvn deploy -s ~/.m2/settings.xml -DaltDeploymentRepository=github::default::https://maven.pkg.github.com/${{ github.repository }} || echo "Deployment failed but continuing..." + env: + YARA_HOME: ${{ env.YARA_HOME }} + + - name: Cleanup + shell: bash + run: rm -rf ~/.m2/settings.xml + bundle: - needs: [build_linux, build_mac] + needs: [build_linux, build_mac, build_windows64] runs-on: ubuntu-latest steps: @@ -190,15 +267,22 @@ jobs: name: osx64-artifacts path: build/osx64 + - name: Download windows64 build artifacts + uses: actions/download-artifact@v4 + with: + name: windows64-artifacts + path: build/windows64 + - name: List current working directory run: ls -lhRt - # Step to bundle Linux and macOS JARs + # Step to bundle Linux, macOS, and Windows JARs - name: Bundle multiplatform JARs run: | mkdir -p bundled-jars cp build/linux64/*.jar bundled-jars/ cp build/osx64/*.jar bundled-jars/ + cp build/windows64/*.jar bundled-jars/ - name: Upload bundled JARs uses: actions/upload-artifact@v4 diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml index 5f1cb86..7658e28 100644 --- a/.github/workflows/ci_release.yml +++ b/.github/workflows/ci_release.yml @@ -179,8 +179,85 @@ jobs: outputs: mac-artifact: ${{ steps.build.outputs.maven-artifact }} + build_windows64: + runs-on: windows-latest + + strategy: + matrix: + jdk: [21] + libyara_tag: [ "v4.5.2-subreption" ] + arch: [windows64] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: ${{ matrix.jdk }} + settings-path: ${{ github.workspace }} + + - name: Set up MSVC developer environment (x64) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Set up NuGet + uses: NuGet/setup-nuget@v2 + + - name: Clone libyara (${{ matrix.libyara_tag }}) + uses: actions/checkout@v4 + with: + repository: subreption/yara + path: yara + ref: ${{ matrix.libyara_tag }} + token: ${{ secrets.GH_YARA_REPO_PAT }} + + # packages.config is not auto-restored by MSBuild. + - name: Restore libyara NuGet packages + run: nuget restore yara/windows/vs2017/yara.sln + + # libyara only (JNI embedded; no yara/yarac). + - name: Build libyara (MSVC Release|x64) + run: | + msbuild "yara/windows/vs2017/libyara/libyara.vcxproj" /p:Configuration=Release /p:Platform=x64 /v:minimal + if ($LASTEXITCODE -ne 0) { throw "msbuild failed for libyara" } + + - name: Export YARA_HOME + run: echo "YARA_HOME=${{ github.workspace }}\yara" >> $env:GITHUB_ENV + + - name: Build YARA-Java (Windows) + run: mvn -B package --file pom.xml + env: + YARA_HOME: ${{ env.YARA_HOME }} + + - name: Upload windows64 build artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.arch }}-artifacts + path: target/*-windows64.jar + overwrite: true + + - name: Set up Maven settings.xml for GitHub Packages + shell: bash + run: | + mkdir -p ~/.m2 + echo "github${{ github.actor }}${{ secrets.GITHUB_TOKEN }}" > ~/.m2/settings.xml + + - name: Deploy Maven artifacts + shell: bash + run: mvn deploy -s ~/.m2/settings.xml -DaltDeploymentRepository=github::default::https://maven.pkg.github.com/${{ github.repository }} || echo "Deployment failed but continuing..." + env: + YARA_HOME: ${{ env.YARA_HOME }} + + - name: Cleanup + shell: bash + run: rm -rf ~/.m2/settings.xml + bundle: - needs: [build_linux64, build_osx64] + needs: [build_linux64, build_osx64, build_windows64] runs-on: ubuntu-latest steps: @@ -196,12 +273,19 @@ jobs: name: osx64-artifacts path: build/osx64 - # Step to bundle Linux and macOS JARs + - name: Download windows64 build artifacts + uses: actions/download-artifact@v4 + with: + name: windows64-artifacts + path: build/windows64 + + # Step to bundle Linux, macOS, and Windows JARs - name: Bundle multiplatform JARs run: | mkdir -p bundled-jars cp build/linux64/*.jar bundled-jars/ cp build/osx64/*.jar bundled-jars/ + cp build/windows64/*.jar bundled-jars/ - name: Upload bundled JARs uses: actions/upload-artifact@v4 From 892ee442367148a1b5c5686528c33bf5219dbbff Mon Sep 17 00:00:00 2001 From: lpendergrass Date: Sat, 11 Jul 2026 20:42:30 -0500 Subject: [PATCH 4/4] Windows ReadMe updates --- README.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cf52e4b..67138ea 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Build (linux64)](https://github.com/subreption/yara-java/actions/workflows/build_linux64.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/build_linux64.yml) [![Build (multiplatform)](https://github.com/subreption/yara-java/actions/workflows/build_multi.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/build_multi.yml) [![Build (osx64)](https://github.com/subreption/yara-java/actions/workflows/build_macos.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/build_macos.yml) [![Nightly Release (multiplatform)](https://github.com/subreption/yara-java/actions/workflows/ci_nightly.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/ci_nightly.yml) [![Stable Release multiplatform](https://github.com/subreption/yara-java/actions/workflows/ci_release.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/ci_release.yml) +[![Build (linux64)](https://github.com/subreption/yara-java/actions/workflows/build_linux64.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/build_linux64.yml) [![Build (multiplatform)](https://github.com/subreption/yara-java/actions/workflows/build_multi.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/build_multi.yml) [![Build (osx64)](https://github.com/subreption/yara-java/actions/workflows/build_macos.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/build_macos.yml) [![Build (windows64)](https://github.com/subreption/yara-java/actions/workflows/build_windows64.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/build_windows64.yml) [![Nightly Release (multiplatform)](https://github.com/subreption/yara-java/actions/workflows/ci_nightly.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/ci_nightly.yml) [![Stable Release multiplatform](https://github.com/subreption/yara-java/actions/workflows/ci_release.yml/badge.svg)](https://github.com/subreption/yara-java/actions/workflows/ci_release.yml) ## Introduction @@ -62,6 +62,22 @@ cd yara-java mvn clean install ``` +### Windows x64 (MSVC) + +On Windows, libyara is built with MSVC from the Visual Studio solution in subreption/yara. From a +*x64 Native Tools Command Prompt* (or after running `vcvars64.bat`): + +``` +git clone https://github.com/subreption/yara.git +cd yara +git checkout tags/v4.5.2-subreption +nuget restore windows/vs2017/yara.sln +msbuild windows/vs2017/libyara/libyara.vcxproj /p:Configuration=Release /p:Platform=x64 +set YARA_HOME=%CD% +``` + +Then build yara-java as above (`mvn clean install`); the `windows-amd64` profile activates automatically. + ### Building in hardened environments It is preferable, especially in hardened environments (where `/tmp` might not be executable or even @@ -75,8 +91,13 @@ extended support. ## Releases -We have added CI workflows to generate *jars* for the supported platforms upon every stable *tag* in this -repository. +CI workflows have been added to generate *jars* for the supported platforms (`linux64`, `osx64`, `windows64`) +upon every stable *tag* in this repository. + +The `windows64` native library is built with MSVC and links the Microsoft Visual C++ runtime dynamically. +On Windows 10/11 no action is required — the JRE provides that runtime (`vcruntime140.dll`) in its own `bin` +directory. On older Windows (7/8) lacking the Universal C Runtime, the Visual C++ Redistributable (2015-2022) +or the Universal C Runtime update (KB2999226) may be required. ## Using the library