Skip to content
Merged
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
1 change: 1 addition & 0 deletions include/naab/governance.h
Original file line number Diff line number Diff line change
Expand Up @@ -3006,6 +3006,7 @@ class GovernanceEngine {
std::string last_audit_hash_;
mutable std::string last_telemetry_hash_;
mutable std::mutex audit_mutex_;
std::atomic<int> audit_write_failures_{0};

// Telemetry forwarding (webhook/SIEM)
mutable std::shared_ptr<TelemetryForwarder> telemetry_forwarder_;
Expand Down
2 changes: 2 additions & 0 deletions include/naab/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ class Environment {
std::shared_ptr<Environment> shallowCopy() const {
auto copy = std::make_shared<Environment>();
copy->values_ = values_; // copy the map
copy->exported_structs_ = exported_structs_; // async needs struct types
copy->exported_enums_ = exported_enums_; // async needs enum types
// Don't copy parent_ — async gets a flat snapshot of globals
return copy;
}
Expand Down
5 changes: 2 additions & 3 deletions src/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1258,9 +1258,8 @@ int main(int argc, char** argv) {
// Set default config for SandboxManager
naab::security::SandboxManager::instance().setDefaultConfig(security_config);

// Configure Python import blocking based on sandbox level
// NOTE: Temporarily disabled while using pure C API (PythonCExecutor)
// TODO: Re-implement import blocking in PythonCExecutor for security
// Python import blocking: enforced at runtime in PythonCExecutor::executeWithReturn()
// via __import__ hook that checks govern.json languages.python.imports.blocked

if (verbose) {
fmt::print("[Security] Sandbox level: {}, timeout: {}s, memory: {}MB, network: {}\n",
Expand Down
20 changes: 17 additions & 3 deletions src/packages/package_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,21 @@ const std::string PackageManager::REGISTRY_URL =
// HTTP Helpers (using libcurl)
// ============================================================================

// Maximum HTTP response size for in-memory string responses (50 MB)
static constexpr size_t MAX_HTTP_RESPONSE_SIZE = 50 * 1024 * 1024;

struct StringWriteContext {
std::string* str;
size_t max_size;
};

static size_t writeStringCallback(void* contents, size_t size, size_t nmemb, void* userp) {
size_t total = size * nmemb;
auto* str = static_cast<std::string*>(userp);
str->append(static_cast<char*>(contents), total);
auto* ctx = static_cast<StringWriteContext*>(userp);
if (ctx->str->size() + total > ctx->max_size) {
return 0; // tells curl to abort with CURLE_WRITE_ERROR
}
ctx->str->append(static_cast<char*>(contents), total);
return total;
}

Expand All @@ -58,6 +69,7 @@ std::string PackageManager::httpGet(const std::string& url) {
}

std::string response;
StringWriteContext write_ctx{&response, MAX_HTTP_RESPONSE_SIZE};
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "User-Agent: NAAb-PackageManager/1.0");
headers = curl_slist_append(headers, "Accept: application/vnd.github.v3+json");
Expand All @@ -71,10 +83,12 @@ std::string PackageManager::httpGet(const std::string& url) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &write_ctx);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE,
static_cast<curl_off_t>(MAX_HTTP_RESPONSE_SIZE));

CURLcode res = curl_easy_perform(curl);
long http_code = 0;
Expand Down
21 changes: 17 additions & 4 deletions src/runtime/block_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,23 @@ std::string BlockRegistry::getBlockSource(const std::string& block_id) const {
// If no inline code, check for code_file reference
if (source.empty() && block_json.contains("code_file") && block_json["code_file"].is_string()) {
std::string code_file = block_json["code_file"].get<std::string>();
// Resolve relative to the JSON file's directory
std::string dir = file_path.substr(0, file_path.find_last_of('/'));
std::string code_path = dir + "/" + code_file;
source = readFile(code_path);
// V-RT-005: Validate code_file is a simple filename (no path traversal)
if (!code_file.empty() &&
code_file[0] != '/' &&
code_file.find("..") == std::string::npos &&
code_file.find('/') == std::string::npos &&
code_file.find('\\') == std::string::npos) {
// Resolve relative to the JSON file's directory
std::string dir = file_path.substr(0, file_path.find_last_of('/'));
std::string code_path = dir + "/" + code_file;
// Double-check: canonical path must stay within block directory
std::error_code ec;
auto canonical_code = std::filesystem::canonical(code_path, ec);
auto canonical_dir = std::filesystem::canonical(dir, ec);
if (!ec && canonical_code.string().rfind(canonical_dir.string(), 0) == 0) {
source = readFile(code_path);
}
}
}
} catch (const std::exception& e) {
return "";
Expand Down
17 changes: 16 additions & 1 deletion src/runtime/governance_reports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,23 @@ void GovernanceEngine::logAuditEvent(const std::string& event_type,
std::ofstream ofs(output_file, std::ios::app);
if (ofs.is_open()) {
ofs << entry.dump() << "\n";
if (ofs.fail()) {
audit_write_failures_.fetch_add(1, std::memory_order_relaxed);
fmt::print(stderr, "[governance] AUDIT WRITE FAILURE: write to {} failed\n",
output_file);
}
} else {
audit_write_failures_.fetch_add(1, std::memory_order_relaxed);
fmt::print(stderr, "[governance] AUDIT WRITE FAILURE: cannot open {}\n",
output_file);
}
} catch (...) {}
} catch (const std::exception& e) {
audit_write_failures_.fetch_add(1, std::memory_order_relaxed);
fmt::print(stderr, "[governance] AUDIT WRITE FAILURE: {}\n", e.what());
} catch (...) {
audit_write_failures_.fetch_add(1, std::memory_order_relaxed);
fmt::print(stderr, "[governance] AUDIT WRITE FAILURE: unknown error\n");
}
}

void GovernanceEngine::logPolyglotExecution(const std::string& language,
Expand Down
31 changes: 30 additions & 1 deletion src/runtime/python_c_executor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "naab/interpreter.h"
#include "naab/sandbox.h"
#include "naab/subprocess_helpers.h" // V-SC-006-ext: env scrub policy
#include "naab/governance.h" // Import blocking: blocked imports from govern.json
#include <stdexcept>
#include <sstream>
#include <string>
Expand Down Expand Up @@ -213,12 +214,40 @@ interpreter::NaabVal PythonCExecutor::executeWithReturn(const std::string& code)
}
#endif

// Import blocking: override __import__ to enforce govern.json blocked imports
// Catches __import__("os"), importlib.import_module("os"), and all runtime import paths
{
auto* engine = governance::GovernanceEngine::getCurrent();
if (engine) {
const auto* lang_cfg = engine->getLanguageConfig("python");
if (lang_cfg && !lang_cfg->imports.blocked.empty()) {
std::ostringstream hook;
hook << "import builtins as _naab_builtins\n"
<< "_naab_original_import = _naab_builtins.__import__\n"
<< "_naab_blocked_modules = {";
for (size_t i = 0; i < lang_cfg->imports.blocked.size(); ++i) {
if (i > 0) hook << ",";
hook << "'" << lang_cfg->imports.blocked[i] << "'";
}
hook << "}\n"
<< "def _naab_safe_import(name, *args, **kwargs):\n"
<< " _top = name.split('.')[0]\n"
<< " if _top in _naab_blocked_modules:\n"
<< " raise ImportError('Import blocked by governance policy: ' + name)\n"
<< " return _naab_original_import(name, *args, **kwargs)\n"
<< "_naab_builtins.__import__ = _naab_safe_import\n"
<< "del _naab_builtins\n";
PyRun_SimpleString(hook.str().c_str());
}
}
}

// Helper lambda: restore stdout and get captured output
auto captureAndRestoreStdout = [&globals, env_scrub_applied]() -> std::string {
// V-SC-006-ext: Restore scrubbed env vars
if (env_scrub_applied) {
PyRun_SimpleString(
"import os as _naab_os\n"
"_naab_os = _naab_original_import('os') if '_naab_original_import' in dir() else __import__('os')\n"
"_naab_os.environ.update(_naab_saved_env)\n"
"del _naab_saved_env, _naab_os\n"
);
Expand Down
20 changes: 15 additions & 5 deletions src/stdlib/agent_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2918,8 +2918,16 @@ static std::pair<std::string, int> validateHandle(NaabVal& handle_val) {
" Help:\n - Use the handle returned by agent.create()\n");
}

int handle_id = handle.at("id").asInt();
std::string config_name = handle.at("config_name").asString();
auto id_it = handle.find("id");
auto cn_it = handle.find("config_name");
if (id_it == handle.end() || !id_it->second.isInt() ||
cn_it == handle.end() || !cn_it->second.isString()) {
throw std::runtime_error(
"Agent error: Invalid agent handle\n\n"
" Help:\n - Use the handle returned by agent.create()\n");
}
int handle_id = id_it->second.asInt();
std::string config_name = cn_it->second.asString();

// Verify HMAC nonce — prevents handle forgery and replay
auto nonce_it = handle.find("__nonce");
Expand Down Expand Up @@ -3216,13 +3224,15 @@ static NaabVal agentPipeline(std::vector<NaabVal>& args) {
std::vector<NaabVal> send_args = {handles[i], NaabVal::makeString(current_message)};
try {
last_response = agentSend(send_args);
} catch (const governance::GovernanceHardError&) {
throw; // HARD blocks propagate without coherence recovery
} catch (...) {
// Recover coherence for the failed stage to prevent floor-grinding
auto* ge = governance::GovernanceEngine::getCurrent();
if (ge && ge->isActive() && handles[i].isDict()) {
auto& d = handles[i].asDict();
auto hid = d.find("id");
if (hid != d.end()) {
if (hid != d.end() && hid->second.isInt()) {
ge->recoverCoherence(hid->second.asInt());
}
}
Expand Down Expand Up @@ -3281,7 +3291,7 @@ static NaabVal agentPipeline(std::vector<NaabVal>& args) {
if (ge && ge->isActive() && handles[i + 1].isDict()) {
auto& next_dict = handles[i + 1].asDict();
auto hid_it = next_dict.find("id");
if (hid_it != next_dict.end()) {
if (hid_it != next_dict.end() && hid_it->second.isInt()) {
ge->setInheritedPressure(hid_it->second.asInt(), stage_pressure);
}
}
Expand All @@ -3294,7 +3304,7 @@ static NaabVal agentPipeline(std::vector<NaabVal>& args) {
if (ge && ge->isActive() && handles[i + 1].isDict()) {
auto& next_dict = handles[i + 1].asDict();
auto hid_it = next_dict.find("id");
if (hid_it != next_dict.end()) {
if (hid_it != next_dict.end() && hid_it->second.isInt()) {
ge->recoverCoherence(hid_it->second.asInt());
}
}
Expand Down
Loading