From 5b8be440963cdcd096ea83a6c17fcfed8e41c6ad Mon Sep 17 00:00:00 2001 From: Termux User Date: Wed, 3 Jun 2026 11:29:58 -0400 Subject: [PATCH 1/5] fix: Python subprocess signing bug + binding CI workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python binding's _subprocess_scan() wrote temp config files that lacked Ed25519 .sig files, causing "INTEGRITY BLOCK" when trusted keys are installed. Fix: add --config-string flag to naab-gov CLI (uses loadFromString, no file-based signature needed) and update Python binding to pass config inline. Also adds CI workflow (.github/workflows/bindings.yml) that builds the shared library and runs binding tests for Go, Rust, Java, C#, and Python in parallel. Includes new test suites for Rust (7 tests), Java (7 tests), and C# (7 tests via xUnit). Security review fixes applied: - Removed bypass hint from help text - --config and --config-string are mutually exclusive (exit 4) - Threat model documented in code comment - Tautological test assertions fixed (>= 0 → > 0) - Java double-close test wrapped in try/catch - Added config enforcement assertion to CLI tests Co-Authored-By: Claude Opus 4.6 --- .github/workflows/bindings.yml | 225 ++++++++++++++++++ .../GovernanceEngineTests.cs | 77 ++++++ .../NaabGovernance.Tests.csproj | 15 ++ .../naab/governance/GovernanceEngineTest.java | 118 +++++++++ bindings/python/naab_governance.py | 31 +-- bindings/rust/tests/governance_test.rs | 76 ++++++ src/cli/gov_main.cpp | 26 +- tests/cli/test_naab_gov.sh | 26 ++ 8 files changed, 571 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/bindings.yml create mode 100644 bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs create mode 100644 bindings/csharp/NaabGovernance.Tests/NaabGovernance.Tests.csproj create mode 100644 bindings/java/src/test/java/org/naab/governance/GovernanceEngineTest.java create mode 100644 bindings/rust/tests/governance_test.rs diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml new file mode 100644 index 00000000..42fde260 --- /dev/null +++ b/.github/workflows/bindings.yml @@ -0,0 +1,225 @@ +name: Bindings + +on: + push: + branches: [ master ] + paths: + - 'bindings/**' + - 'include/naab/public/**' + - 'src/api/**' + - 'CMakeLists.txt' + pull_request: + branches: [ master ] + paths: + - 'bindings/**' + - 'include/naab/public/**' + - 'src/api/**' + - 'CMakeLists.txt' + workflow_dispatch: + +jobs: + build-shared-lib: + name: Build libnaab-governance + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build libsqlite3-dev python3-dev \ + libssl-dev libffi-dev libcurl4-openssl-dev pkg-config + + - name: Build shared library + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Release -G Ninja + ninja -C build naab_governance naab-gov -j$(nproc) + # Verify shared lib was built + ls -la build/libnaab-governance.so* + + - name: Upload shared library + uses: actions/upload-artifact@v4 + with: + name: libnaab-governance + path: | + build/libnaab-governance.so* + build/naab-gov + retention-days: 1 + + test-go: + name: Go Bindings + needs: build-shared-lib + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Download shared library + uses: actions/download-artifact@v4 + with: + name: libnaab-governance + + - name: Install shared library + run: | + sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo ldconfig + + - name: Run Go tests + working-directory: bindings/go + run: | + CGO_LDFLAGS="-L/usr/local/lib" \ + go test -v ./naabgov/ + + test-rust: + name: Rust Bindings + needs: build-shared-lib + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - name: Download shared library + uses: actions/download-artifact@v4 + with: + name: libnaab-governance + + - name: Install shared library + run: | + sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo ldconfig + + - name: Run Rust tests + working-directory: bindings/rust + run: | + NAAB_LIB_DIR=/usr/local/lib cargo test --verbose + + test-java: + name: Java Bindings + needs: build-shared-lib + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Download shared library + uses: actions/download-artifact@v4 + with: + name: libnaab-governance + + - name: Install shared library + run: | + sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo ldconfig + + - name: Build JNI bridge + working-directory: bindings/java + run: | + # Compile Java source + javac -d classes src/main/java/org/naab/governance/GovernanceEngine.java + + # Build JNI shared library + cc -shared -fPIC -o libnaab-governance-jni.so \ + src/main/c/naab_gov_jni.c \ + -I${JAVA_HOME}/include \ + -I${JAVA_HOME}/include/linux \ + -I../../include/naab/public \ + -L/usr/local/lib \ + -lnaab-governance + + sudo cp libnaab-governance-jni.so /usr/local/lib/ + sudo ldconfig + + - name: Compile tests + working-directory: bindings/java + run: | + javac -d classes -cp classes \ + src/test/java/org/naab/governance/GovernanceEngineTest.java + + - name: Run Java tests + working-directory: bindings/java + run: | + java -cp classes \ + -Djava.library.path=/usr/local/lib \ + org.naab.governance.GovernanceEngineTest + + test-csharp: + name: C# Bindings + needs: build-shared-lib + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0' + + - name: Download shared library + uses: actions/download-artifact@v4 + with: + name: libnaab-governance + + - name: Install shared library + run: | + sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo ldconfig + + - name: Run C# tests + working-directory: bindings/csharp + run: | + LD_LIBRARY_PATH=/usr/local/lib dotnet test NaabGovernance.Tests/ --verbosity normal + + test-python: + name: Python Bindings (subprocess) + needs: build-shared-lib + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Download shared library + uses: actions/download-artifact@v4 + with: + name: libnaab-governance + + - name: Make naab-gov executable + run: chmod +x build/naab-gov + + - name: Run Python binding smoke test + run: | + NAAB_GOV_BIN=./build/naab-gov python3 -c " + import sys, os + sys.path.insert(0, 'bindings/python') + os.environ['NAAB_GOV_LIB'] = '/nonexistent' + os.environ['NAAB_GOV_BIN'] = os.path.abspath('./build/naab-gov') + from naab_governance import GovernanceEngine + + eng = GovernanceEngine() + assert eng._subprocess_mode, 'should be in subprocess mode' + + # Test with inline config (validates --config-string fix) + eng.load_config_dict({'version': '5.0', 'mode': 'enforce', + 'restrictions': {'dangerous_calls': {'level': 'hard'}}}) + + result = eng.scan('python', 'x = 42') + assert not result.get('blocked'), 'safe code should not be blocked' + + result = eng.scan('python', 'import os; os.system(\"rm -rf /\")') + # May or may not block depending on config specifics, just check it runs + assert 'blocked' in result, 'result should have blocked field' + + print('Python subprocess binding: OK') + " diff --git a/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs b/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs new file mode 100644 index 00000000..2aa84507 --- /dev/null +++ b/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs @@ -0,0 +1,77 @@ +using Xunit; +using NaabGovernance; + +namespace NaabGovernance.Tests; + +public class GovernanceEngineTests +{ + private const string TestConfig = + @"{""version"":""3.0"",""mode"":""enforce"", + ""restrictions"":{""dangerous_calls"":{""level"":""hard""}, + ""code_injection"":{""level"":""hard""}}, + ""code_quality"":{""no_secrets"":{""level"":""hard""}}}"; + + [Fact] + public void Version_ReturnsNonEmpty() + { + var v = GovernanceEngine.Version(); + Assert.False(string.IsNullOrEmpty(v)); + } + + [Fact] + public void Lifecycle_CreateAndDispose() + { + using var engine = new GovernanceEngine(); + Assert.NotNull(engine); + } + + [Fact] + public void Lifecycle_DoubleDispose() + { + var engine = new GovernanceEngine(); + engine.Dispose(); + engine.Dispose(); // should not throw + } + + [Fact] + public void ScanSafeCode_NotBlocked() + { + using var engine = new GovernanceEngine(); + engine.LoadConfigString(TestConfig); + Assert.True(engine.IsActive); + + var result = engine.Scan("python", "x = 42\nprint(x)", "test.py", 1); + Assert.NotNull(result); + Assert.False(engine.WasBlocked); + } + + [Fact] + public void ScanDangerousCode_Blocked() + { + using var engine = new GovernanceEngine(); + engine.LoadConfigString(TestConfig); + + engine.Scan("python", "import os; os.system('rm -rf /')", "test.py", 1); + Assert.True(engine.WasBlocked); + } + + [Fact] + public void Reset_ClearsResults() + { + using var engine = new GovernanceEngine(); + engine.LoadConfigString(TestConfig); + + engine.Scan("python", "eval(input())", "test.py", 1); + Assert.True(engine.ResultCount > 0); + + engine.Reset(); + Assert.Equal(0, engine.ResultCount); + } + + [Fact] + public void IsActive_FalseBeforeConfig() + { + using var engine = new GovernanceEngine(); + Assert.False(engine.IsActive); + } +} diff --git a/bindings/csharp/NaabGovernance.Tests/NaabGovernance.Tests.csproj b/bindings/csharp/NaabGovernance.Tests/NaabGovernance.Tests.csproj new file mode 100644 index 00000000..ff1a6320 --- /dev/null +++ b/bindings/csharp/NaabGovernance.Tests/NaabGovernance.Tests.csproj @@ -0,0 +1,15 @@ + + + net8.0 + false + true + + + + + + + + + + diff --git a/bindings/java/src/test/java/org/naab/governance/GovernanceEngineTest.java b/bindings/java/src/test/java/org/naab/governance/GovernanceEngineTest.java new file mode 100644 index 00000000..dc3280e0 --- /dev/null +++ b/bindings/java/src/test/java/org/naab/governance/GovernanceEngineTest.java @@ -0,0 +1,118 @@ +package org.naab.governance; + +/** + * Tests for NAAb governance JNI bindings. + * + * Runs standalone (no JUnit dependency) — each test method prints PASS/FAIL. + * Exit code 0 = all pass, 1 = any fail. + */ +public class GovernanceEngineTest { + + private static final String TEST_CONFIG = + "{\"version\":\"3.0\",\"mode\":\"enforce\"," + + "\"restrictions\":{\"dangerous_calls\":{\"level\":\"hard\"}," + + "\"code_injection\":{\"level\":\"hard\"}}," + + "\"code_quality\":{\"no_secrets\":{\"level\":\"hard\"}}}"; + + private static int passed = 0; + private static int failed = 0; + + private static void check(String name, boolean condition) { + if (condition) { + System.out.println(" PASS: " + name); + passed++; + } else { + System.out.println(" FAIL: " + name); + failed++; + } + } + + public static void main(String[] args) { + System.out.println(); + System.out.println("--- Java JNI Binding Tests ---"); + System.out.println(); + + testVersion(); + testLifecycle(); + testScanSafeCode(); + testScanDangerousCode(); + testReset(); + testIsActive(); + testResultCount(); + + System.out.println(); + System.out.println("Results: " + passed + "/" + (passed + failed) + " passed"); + System.out.println(); + System.exit(failed > 0 ? 1 : 0); + } + + private static void testVersion() { + String v = GovernanceEngine.version(); + check("version not null", v != null); + check("version not empty", v != null && !v.isEmpty()); + } + + private static void testLifecycle() { + try (GovernanceEngine engine = new GovernanceEngine()) { + check("create engine", engine != null); + } + // double close via try-with-resources then explicit — should not crash + boolean doubleCloseOk = false; + try { + GovernanceEngine engine = new GovernanceEngine(); + engine.close(); + engine.close(); + doubleCloseOk = true; + } catch (Exception e) { + // native crash that propagates as Java exception + } + check("double close safe", doubleCloseOk); + } + + private static void testScanSafeCode() { + try (GovernanceEngine engine = new GovernanceEngine()) { + engine.loadConfigString(TEST_CONFIG); + check("isActive after config", engine.isActive()); + + String result = engine.scan("python", "x = 42\nprint(x)", "test.py", 1); + check("scan returns JSON", result != null && result.contains("blocked")); + check("safe code not blocked", !engine.wasBlocked()); + } + } + + private static void testScanDangerousCode() { + try (GovernanceEngine engine = new GovernanceEngine()) { + engine.loadConfigString(TEST_CONFIG); + engine.scan("python", "import os; os.system('rm -rf /')", "test.py", 1); + check("dangerous code blocked", engine.wasBlocked()); + } + } + + private static void testReset() { + try (GovernanceEngine engine = new GovernanceEngine()) { + engine.loadConfigString(TEST_CONFIG); + engine.scan("python", "eval(input())", "test.py", 1); + check("has results after scan", engine.resultCount() > 0); + + engine.reset(); + check("no results after reset", engine.resultCount() == 0); + } + } + + private static void testIsActive() { + try (GovernanceEngine engine = new GovernanceEngine()) { + check("not active before config", !engine.isActive()); + engine.loadConfigString(TEST_CONFIG); + check("active after config", engine.isActive()); + } + } + + private static void testResultCount() { + try (GovernanceEngine engine = new GovernanceEngine()) { + engine.loadConfigString(TEST_CONFIG); + check("zero results before scan", engine.resultCount() == 0); + engine.scan("python", "eval(input())", "test.py", 1); + check("has results after scan", engine.resultCount() > 0); + } + } +} diff --git a/bindings/python/naab_governance.py b/bindings/python/naab_governance.py index ab92b422..03884fa4 100644 --- a/bindings/python/naab_governance.py +++ b/bindings/python/naab_governance.py @@ -244,29 +244,16 @@ def _subprocess_scan(self, language: str, code: str, config=None) -> dict: """Run governance scan via naab-gov CLI subprocess.""" cmd = [self._subprocess_bin, "check", "--language", language] if config: - # Write temp config file - import tempfile - tmpdir = os.environ.get("TMPDIR", "/tmp") - cfg_fd = tempfile.NamedTemporaryFile( - mode="w", delete=False, dir=tmpdir, suffix=".json", prefix="naab_gov_" - ) - cfg_path = cfg_fd.name - try: - json.dump(config if isinstance(config, dict) else json.loads(config), cfg_fd) - cfg_fd.close() - cmd.extend(["--config", cfg_path]) - proc = subprocess.run( - cmd, input=code, capture_output=True, text=True, timeout=30 - ) - finally: - try: - os.unlink(cfg_path) - except OSError: - pass - else: - proc = subprocess.run( - cmd, input=code, capture_output=True, text=True, timeout=30 + # Pass config inline via --config-string to avoid signature + # verification issues with temp files (Ed25519 .sig required + # when trusted keys are installed) + cfg_json = json.dumps( + config if isinstance(config, dict) else json.loads(config) ) + cmd.extend(["--config-string", cfg_json]) + proc = subprocess.run( + cmd, input=code, capture_output=True, text=True, timeout=30 + ) if proc.returncode == 1: raise RuntimeError(f"naab-gov check failed: {proc.stderr.strip()}") if proc.returncode == 4: diff --git a/bindings/rust/tests/governance_test.rs b/bindings/rust/tests/governance_test.rs new file mode 100644 index 00000000..62432f4d --- /dev/null +++ b/bindings/rust/tests/governance_test.rs @@ -0,0 +1,76 @@ +use naab_governance::{GovernanceEngine, version}; + +const TEST_CONFIG: &str = r#"{ + "version": "3.0", + "mode": "enforce", + "restrictions": { + "dangerous_calls": {"level": "hard"}, + "code_injection": {"level": "hard"} + }, + "code_quality": { + "no_secrets": {"level": "hard"} + } +}"#; + +#[test] +fn test_version() { + let v = version(); + assert!(!v.is_empty(), "version string should not be empty"); +} + +#[test] +fn test_lifecycle() { + let engine = GovernanceEngine::new().expect("create engine"); + drop(engine); // explicit drop should not panic +} + +#[test] +fn test_scan_safe_code() { + let engine = GovernanceEngine::new().unwrap(); + engine.load_config_string(TEST_CONFIG).unwrap(); + assert!(engine.is_active(), "engine should be active after loading config"); + + let result = engine.scan("python", "x = 42\nprint(x)", "test.py", 1).unwrap(); + assert!(!result.blocked, "safe code should not be blocked"); +} + +#[test] +fn test_scan_dangerous_code() { + let engine = GovernanceEngine::new().unwrap(); + engine.load_config_string(TEST_CONFIG).unwrap(); + + let result = engine.scan("python", "import os; os.system('rm -rf /')", "test.py", 1).unwrap(); + assert!(result.blocked, "dangerous code should be blocked"); +} + +#[test] +fn test_reset() { + let engine = GovernanceEngine::new().unwrap(); + engine.load_config_string(TEST_CONFIG).unwrap(); + + engine.scan("python", "eval(input())", "test.py", 1).unwrap(); + assert!(engine.result_count() > 0, "should have results after scan"); + + engine.reset(); + assert_eq!(engine.result_count(), 0, "should have no results after reset"); +} + +#[test] +fn test_is_active() { + let engine = GovernanceEngine::new().unwrap(); + assert!(!engine.is_active(), "should not be active before loading config"); + + engine.load_config_string(TEST_CONFIG).unwrap(); + assert!(engine.is_active(), "should be active after loading config"); +} + +#[test] +fn test_result_count() { + let engine = GovernanceEngine::new().unwrap(); + engine.load_config_string(TEST_CONFIG).unwrap(); + + assert_eq!(engine.result_count(), 0, "no results before scan"); + + engine.scan("python", "eval(input())", "test.py", 1).unwrap(); + assert!(engine.result_count() > 0, "should have results after scanning dangerous code"); +} diff --git a/src/cli/gov_main.cpp b/src/cli/gov_main.cpp index a62e0560..52e1d94e 100644 --- a/src/cli/gov_main.cpp +++ b/src/cli/gov_main.cpp @@ -72,6 +72,7 @@ static void printHelp() { " --language Target language (required: python, javascript, go, ...)\n" " --file Read code from file instead of stdin\n" " --config Use this govern.json instead of auto-discovery\n" + " --config-string Use inline JSON config string\n" " --sarif Output SARIF instead of JSON\n" " --env Apply named environment overlay from govern.json\n" "\n" @@ -265,6 +266,7 @@ static int cmdCheck(const std::vector& args) { std::string language; std::string file_path; std::string config_path; + std::string config_string; std::string env_name; bool sarif_output = false; @@ -276,6 +278,8 @@ static int cmdCheck(const std::vector& args) { file_path = args[++i]; } else if (args[i] == "--config" && i + 1 < args.size()) { config_path = args[++i]; + } else if (args[i] == "--config-string" && i + 1 < args.size()) { + config_string = args[++i]; } else if (args[i] == "--env" && i + 1 < args.size()) { env_name = args[++i]; } else if (args[i] == "--sarif") { @@ -286,6 +290,11 @@ static int cmdCheck(const std::vector& args) { } } + if (!config_string.empty() && !config_path.empty()) { + std::cerr << "naab-gov check: --config and --config-string are mutually exclusive\n"; + return 4; + } + if (language.empty()) { std::cerr << "naab-gov check: --language is required\n" "Usage: naab-gov check --language [--file ] [--config govern.json]\n"; @@ -322,10 +331,25 @@ static int cmdCheck(const std::vector& args) { } // Load governance config + // + // Threat model for --config-string: naab-gov is a user-space CLI tool, + // never setuid/setgid, never runs with elevated privileges. The caller + // already has full control of the process (can pass --no-governance to + // naab-lang, or simply not invoke governance at all). --config-string + // exists for programmatic callers (Python subprocess binding) that need + // to pass config without writing temp files that require Ed25519 signing. + // Signature verification gates file-based configs; inline configs are + // trusted as caller-provided input, same as any other CLI argument. naab::governance::GovernanceEngine engine; bool loaded = false; - if (!config_path.empty()) { + if (!config_string.empty()) { + loaded = engine.loadFromString(config_string); + if (!loaded) { + std::cerr << "naab-gov check: failed to parse config string\n"; + return 4; + } + } else if (!config_path.empty()) { if (!fs::exists(config_path)) { std::cerr << "naab-gov check: govern.json not found: " << config_path << "\n"; return 4; diff --git a/tests/cli/test_naab_gov.sh b/tests/cli/test_naab_gov.sh index 11ac86d5..f6cedcab 100644 --- a/tests/cli/test_naab_gov.sh +++ b/tests/cli/test_naab_gov.sh @@ -75,6 +75,32 @@ MISSING_EXIT=$? check "lint non-existent file returns non-zero" \ "$([ "${MISSING_EXIT}" -ne 0 ] && echo 0 || echo 1)" +# Test 6: check --config-string passes JSON inline +CONFIG_STR_OUT=$(echo "x = 1" | "${GOV}" check --language python --config-string '{"version":"5.0","mode":"enforce"}' 2>&1) +CONFIG_STR_EXIT=$? +check "--config-string exits 0" "$CONFIG_STR_EXIT" +check "--config-string returns JSON" \ + "$(echo "${CONFIG_STR_OUT}" | grep -q '"blocked"' && echo 0 || echo 1)" + +# Test 7: check --config-string with invalid JSON returns exit 4 +echo "x = 1" | "${GOV}" check --language python --config-string '{bad json' 2>/dev/null +BADJSON_EXIT=$? +check "--config-string bad JSON exits 4" \ + "$([ "${BADJSON_EXIT}" -eq 4 ] && echo 0 || echo 1)" + +# Test 8: check --config-string actually applies config (dangerous code → exit 3) +echo 'import os; os.system("rm -rf /")' | "${GOV}" check --language python \ + --config-string '{"version":"5.0","mode":"enforce","restrictions":{"dangerous_calls":{"level":"hard"}}}' 2>/dev/null +DANGEROUS_EXIT=$? +check "--config-string enforces config (exit 3)" \ + "$([ "${DANGEROUS_EXIT}" -eq 3 ] && echo 0 || echo 1)" + +# Test 9: --config and --config-string are mutually exclusive (exit 4) +echo "x = 1" | "${GOV}" check --language python --config /dev/null --config-string '{}' 2>/dev/null +MUTEX_EXIT=$? +check "--config + --config-string mutually exclusive (exit 4)" \ + "$([ "${MUTEX_EXIT}" -eq 4 ] && echo 0 || echo 1)" + echo "" echo "Results: ${PASS}/${TESTS} passed" echo "" From ca5c1d7574212110c66819a312c86aa922d03323 Mon Sep 17 00:00:00 2001 From: Termux User Date: Wed, 3 Jun 2026 11:43:08 -0400 Subject: [PATCH 2/5] fix: use CMAKE_EXE_LINKER_FLAGS for -pie to avoid shared lib conflict add_link_options(-pie) applied globally, including to shared library targets. -pie and -shared conflict on the linker, causing "failed to set dynamic section sizes: bad value" when building libnaab-governance as a shared library on Linux CI. Switch to CMAKE_EXE_LINKER_FLAGS so -pie only applies to executables. Co-Authored-By: Claude Opus 4.6 --- CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 82b649a2..9d611a79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,9 +91,11 @@ if(ENABLE_HARDENING AND NOT MSVC) # Stack protection add_compile_options(-fstack-protector-strong) - # Position-independent code (PIE) for ASLR + # Position-independent code (PIE) for ASLR — executables only + # Note: -pie conflicts with -shared, so we use CMAKE_EXE_LINKER_FLAGS + # instead of add_link_options() to avoid breaking shared library builds. add_compile_options(-fPIE) - add_link_options(-pie) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie") # Fortify source (buffer overflow detection) — requires -O1+, inactive in Debug if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") From 7b9824cba1c59d7bb29857190bef0980217db980 Mon Sep 17 00:00:00 2001 From: Termux User Date: Wed, 3 Jun 2026 12:07:51 -0400 Subject: [PATCH 3/5] fix: remove -fPIE compile flag that breaks shared library builds Static libraries compiled with -fPIE use TLS relocations (TPOFF32) incompatible with shared objects. CMAKE_POSITION_INDEPENDENT_CODE=ON already adds -fPIC globally, which is a superset of -fPIE and works for both executables and shared libraries. Only -pie linker flag (scoped to executables) is needed for ASLR. Co-Authored-By: Claude Opus 4.6 --- CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d611a79..76ad428e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,10 +91,10 @@ if(ENABLE_HARDENING AND NOT MSVC) # Stack protection add_compile_options(-fstack-protector-strong) - # Position-independent code (PIE) for ASLR — executables only - # Note: -pie conflicts with -shared, so we use CMAKE_EXE_LINKER_FLAGS - # instead of add_link_options() to avoid breaking shared library builds. - add_compile_options(-fPIE) + # Position-independent code for ASLR + # CMAKE_POSITION_INDEPENDENT_CODE=ON (line 33) already adds -fPIC globally, + # which is a superset of -fPIE. We only need -pie for the linker on executables + # (-pie conflicts with -shared, so we scope it to CMAKE_EXE_LINKER_FLAGS). set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie") # Fortify source (buffer overflow detection) — requires -O1+, inactive in Debug From 312ac8bb920a03039bd882ab73e402d800d76d3b Mon Sep 17 00:00:00 2001 From: Termux User Date: Wed, 3 Jun 2026 12:18:34 -0400 Subject: [PATCH 4/5] fix: CI artifact paths and shared lib build flags - Remove -fPIE (conflicts with -fPIC for shared libs; TPOFF32 relocs) - Copy real .so file (not symlinks) into artifacts directory - Download artifacts to build/ directory in downstream jobs - Disable Go module cache (pure CGO, no go.sum) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/bindings.yml | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml index 42fde260..a8080027 100644 --- a/.github/workflows/bindings.yml +++ b/.github/workflows/bindings.yml @@ -40,13 +40,20 @@ jobs: # Verify shared lib was built ls -la build/libnaab-governance.so* + - name: Prepare artifacts + run: | + mkdir -p artifacts + # Copy the real .so file (not symlinks — artifacts don't preserve them) + cp build/libnaab-governance.so.*.* artifacts/libnaab-governance.so 2>/dev/null || \ + cp build/libnaab-governance.so artifacts/libnaab-governance.so + cp build/naab-gov artifacts/ + ls -la artifacts/ + - name: Upload shared library uses: actions/upload-artifact@v4 with: name: libnaab-governance - path: | - build/libnaab-governance.so* - build/naab-gov + path: artifacts/ retention-days: 1 test-go: @@ -59,15 +66,17 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '1.21' + cache: false - name: Download shared library uses: actions/download-artifact@v4 with: name: libnaab-governance + path: build - name: Install shared library run: | - sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo cp build/libnaab-governance.so /usr/local/lib/ sudo ldconfig - name: Run Go tests @@ -89,10 +98,11 @@ jobs: uses: actions/download-artifact@v4 with: name: libnaab-governance + path: build - name: Install shared library run: | - sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo cp build/libnaab-governance.so /usr/local/lib/ sudo ldconfig - name: Run Rust tests @@ -116,10 +126,11 @@ jobs: uses: actions/download-artifact@v4 with: name: libnaab-governance + path: build - name: Install shared library run: | - sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo cp build/libnaab-governance.so /usr/local/lib/ sudo ldconfig - name: Build JNI bridge @@ -168,10 +179,11 @@ jobs: uses: actions/download-artifact@v4 with: name: libnaab-governance + path: build - name: Install shared library run: | - sudo cp build/libnaab-governance.so* /usr/local/lib/ + sudo cp build/libnaab-governance.so /usr/local/lib/ sudo ldconfig - name: Run C# tests @@ -194,9 +206,13 @@ jobs: uses: actions/download-artifact@v4 with: name: libnaab-governance + path: build - - name: Make naab-gov executable - run: chmod +x build/naab-gov + - name: Install shared library and CLI + run: | + sudo cp build/libnaab-governance.so /usr/local/lib/ + sudo ldconfig + chmod +x build/naab-gov - name: Run Python binding smoke test run: | From 5e4698e970f01a66dc822b178806cc9d73eed7f9 Mon Sep 17 00:00:00 2001 From: Termux User Date: Wed, 3 Jun 2026 12:27:54 -0400 Subject: [PATCH 5/5] fix: Python/Java/C# binding CI test failures - Python: test both ctypes and subprocess modes (shared lib is available) - Java JNI: add missing #include for uintptr_t - C#: Version is a property, not a method (remove parens) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/bindings.yml | 32 +++++++++++-------- .../GovernanceEngineTests.cs | 2 +- bindings/java/src/main/c/naab_gov_jni.c | 1 + 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml index a8080027..6e9e886f 100644 --- a/.github/workflows/bindings.yml +++ b/.github/workflows/bindings.yml @@ -216,26 +216,32 @@ jobs: - name: Run Python binding smoke test run: | - NAAB_GOV_BIN=./build/naab-gov python3 -c " + python3 -c " import sys, os sys.path.insert(0, 'bindings/python') - os.environ['NAAB_GOV_LIB'] = '/nonexistent' - os.environ['NAAB_GOV_BIN'] = os.path.abspath('./build/naab-gov') + + # Test 1: ctypes mode (shared lib available) + os.environ['NAAB_GOV_LIB'] = os.path.abspath('build/libnaab-governance.so') from naab_governance import GovernanceEngine eng = GovernanceEngine() - assert eng._subprocess_mode, 'should be in subprocess mode' - - # Test with inline config (validates --config-string fix) - eng.load_config_dict({'version': '5.0', 'mode': 'enforce', - 'restrictions': {'dangerous_calls': {'level': 'hard'}}}) - + assert not eng._subprocess_mode, 'should be in ctypes mode' + eng.load_config_string('{\"version\":\"5.0\",\"mode\":\"enforce\"}') result = eng.scan('python', 'x = 42') assert not result.get('blocked'), 'safe code should not be blocked' + del eng + print('Python ctypes binding: OK') - result = eng.scan('python', 'import os; os.system(\"rm -rf /\")') - # May or may not block depending on config specifics, just check it runs - assert 'blocked' in result, 'result should have blocked field' - + # Test 2: subprocess mode (force by pointing to nonexistent lib) + os.environ['NAAB_GOV_LIB'] = '/nonexistent' + os.environ['NAAB_GOV_BIN'] = os.path.abspath('build/naab-gov') + # Re-import to get fresh instance + import importlib, naab_governance + importlib.reload(naab_governance) + eng2 = naab_governance.GovernanceEngine(lib_path='/nonexistent') + assert eng2._subprocess_mode, 'should be in subprocess mode' + eng2.load_config_dict({'version': '5.0', 'mode': 'enforce'}) + result2 = eng2.scan('python', 'x = 42') + assert 'blocked' in result2, 'result should have blocked field' print('Python subprocess binding: OK') " diff --git a/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs b/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs index 2aa84507..32872ad7 100644 --- a/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs +++ b/bindings/csharp/NaabGovernance.Tests/GovernanceEngineTests.cs @@ -14,7 +14,7 @@ public class GovernanceEngineTests [Fact] public void Version_ReturnsNonEmpty() { - var v = GovernanceEngine.Version(); + var v = GovernanceEngine.Version; Assert.False(string.IsNullOrEmpty(v)); } diff --git a/bindings/java/src/main/c/naab_gov_jni.c b/bindings/java/src/main/c/naab_gov_jni.c index 09fa2d56..7e1184b8 100644 --- a/bindings/java/src/main/c/naab_gov_jni.c +++ b/bindings/java/src/main/c/naab_gov_jni.c @@ -8,6 +8,7 @@ */ #include +#include #include #include "naab_governance.h"