nyx-scanner 0.3.0

A CLI security scanner for automating vulnerability checks
Documentation
use crate::labels::Cap;
use crate::symbol::Lang;

/// A guard rule: functions that must dominate sinks to ensure safety.
pub struct GuardRule {
    pub matchers: &'static [&'static str],
    pub applies_to_sink_caps: Cap,
}

/// An auth rule: functions that perform authentication/authorization checks.
pub struct AuthRule {
    pub matchers: &'static [&'static str],
}

/// An entry point rule: functions that serve as external-facing entry points.
pub struct EntryPointRule {
    pub matchers: &'static [&'static str],
}

/// A resource acquire/release pair.
pub struct ResourcePair {
    pub acquire: &'static [&'static str],
    pub release: &'static [&'static str],
    /// Patterns that look like acquire calls (e.g. `freopen` ends with `fopen`)
    /// but should NOT be treated as acquisitions.
    pub exclude_acquire: &'static [&'static str],
    pub resource_name: &'static str,
}

// ── Guard rules ─────────────────────────────────────────────────────────

static COMMON_GUARDS: &[GuardRule] = &[
    GuardRule {
        matchers: &["validate", "sanitize"],
        applies_to_sink_caps: Cap::all(),
    },
    GuardRule {
        matchers: &["check_", "verify_", "assert_"],
        applies_to_sink_caps: Cap::all(),
    },
    GuardRule {
        matchers: &["shell_escape", "quote", "escape_shell"],
        applies_to_sink_caps: Cap::SHELL_ESCAPE,
    },
    GuardRule {
        matchers: &["html_escape", "encode_safe", "escape_html", "sanitize_html"],
        applies_to_sink_caps: Cap::HTML_ESCAPE,
    },
    GuardRule {
        matchers: &["url_encode", "encode_uri", "urlencode"],
        applies_to_sink_caps: Cap::URL_ENCODE,
    },
    GuardRule {
        matchers: &[
            "which",
            "resolve_binary",
            "find_program",
            "lookup_path",
            "shutil.which",
        ],
        applies_to_sink_caps: Cap::SHELL_ESCAPE,
    },
];

pub fn guard_rules(_lang: Lang) -> &'static [GuardRule] {
    // All languages share the common set for now; per-language
    // overrides can be added via match arms when needed.
    COMMON_GUARDS
}

// ── Auth rules ──────────────────────────────────────────────────────────

static COMMON_AUTH: &[AuthRule] = &[AuthRule {
    matchers: &[
        "is_authenticated",
        "require_auth",
        "check_permission",
        "is_admin",
        "authorize",
        "authenticate",
        "require_login",
        "check_auth",
        "verify_token",
        "validate_token",
    ],
}];

static GO_AUTH: &[AuthRule] = &[AuthRule {
    matchers: &[
        "is_authenticated",
        "require_auth",
        "check_permission",
        "is_admin",
        "authorize",
        "authenticate",
        "require_login",
        "check_auth",
        "verify_token",
        "validate_token",
        "middleware.auth",
        "auth.required",
    ],
}];

static JAVA_AUTH: &[AuthRule] = &[AuthRule {
    matchers: &[
        "is_authenticated",
        "require_auth",
        "check_permission",
        "is_admin",
        "authorize",
        "authenticate",
        "require_login",
        "check_auth",
        "verify_token",
        "validate_token",
        "isAuthenticated",
        "checkPermission",
        "hasAuthority",
        "hasRole",
    ],
}];

pub fn auth_rules(lang: Lang) -> &'static [AuthRule] {
    match lang {
        Lang::Go => GO_AUTH,
        Lang::Java => JAVA_AUTH,
        _ => COMMON_AUTH,
    }
}

// ── Entry point rules ───────────────────────────────────────────────────

static COMMON_ENTRY_POINTS: &[EntryPointRule] = &[EntryPointRule {
    matchers: &[
        "main",
        "handle_*",
        "route_*",
        "api_*",
        "serve_*",
        "process_*",
    ],
}];

static GO_ENTRY_POINTS: &[EntryPointRule] = &[EntryPointRule {
    matchers: &[
        "main",
        "handle_*",
        "handler_*",
        "route_*",
        "api_*",
        "serve_*",
        "process_*",
        "ServeHTTP",
    ],
}];

static PYTHON_ENTRY_POINTS: &[EntryPointRule] = &[EntryPointRule {
    matchers: &[
        "main",
        "handle_*",
        "route_*",
        "api_*",
        "serve_*",
        "process_*",
        "view_*",
    ],
}];

pub fn entry_point_rules(lang: Lang) -> &'static [EntryPointRule] {
    match lang {
        Lang::Go => GO_ENTRY_POINTS,
        Lang::Python => PYTHON_ENTRY_POINTS,
        _ => COMMON_ENTRY_POINTS,
    }
}

// ── Resource pairs ──────────────────────────────────────────────────────

static C_RESOURCES: &[ResourcePair] = &[
    ResourcePair {
        acquire: &["malloc", "calloc", "realloc"],
        release: &["free"],
        exclude_acquire: &[],
        resource_name: "memory",
    },
    ResourcePair {
        acquire: &["fopen", "fdopen", "curlx_fopen", "curlx_fdopen"],
        release: &["fclose", "curlx_fclose"],
        exclude_acquire: &["freopen", "curlx_freopen"],
        resource_name: "file handle",
    },
    ResourcePair {
        acquire: &["open"],
        release: &["close"],
        exclude_acquire: &["freopen", "curlx_freopen"],
        resource_name: "file descriptor",
    },
    ResourcePair {
        acquire: &["pthread_mutex_lock"],
        release: &["pthread_mutex_unlock"],
        exclude_acquire: &[],
        resource_name: "mutex",
    },
];

static GO_RESOURCES: &[ResourcePair] = &[
    ResourcePair {
        acquire: &["os.Open", "os.Create", "os.OpenFile"],
        release: &[".Close"],
        exclude_acquire: &[],
        resource_name: "file handle",
    },
    ResourcePair {
        acquire: &[".Lock"],
        release: &[".Unlock"],
        exclude_acquire: &[],
        resource_name: "mutex",
    },
];

static RUST_RESOURCES: &[ResourcePair] = &[
    // Rust uses RAII, but unsafe alloc/dealloc is a pattern
    ResourcePair {
        acquire: &["alloc"],
        release: &["dealloc"],
        exclude_acquire: &[],
        resource_name: "raw memory",
    },
];

static JAVA_RESOURCES: &[ResourcePair] = &[ResourcePair {
    acquire: &[
        "new FileInputStream",
        "new FileOutputStream",
        "new BufferedReader",
        "openConnection",
    ],
    release: &[".close"],
    exclude_acquire: &[],
    resource_name: "stream/connection",
}];

static PYTHON_RESOURCES: &[ResourcePair] = &[
    ResourcePair {
        acquire: &["open"],
        release: &[".close"],
        exclude_acquire: &[],
        resource_name: "file handle",
    },
    ResourcePair {
        acquire: &["socket.socket", "socket"],
        release: &[".close"],
        exclude_acquire: &[],
        resource_name: "socket",
    },
    ResourcePair {
        acquire: &["connect", "cursor"],
        release: &[".close"],
        exclude_acquire: &["signal.connect", "event.connect", ".register"],
        resource_name: "db connection",
    },
    ResourcePair {
        acquire: &["threading.Lock", "threading.RLock"],
        release: &[".release"],
        exclude_acquire: &[],
        resource_name: "mutex",
    },
];

static RUBY_RESOURCES: &[ResourcePair] = &[
    ResourcePair {
        acquire: &["File.open", "open"],
        release: &[".close"],
        exclude_acquire: &[],
        resource_name: "file handle",
    },
    ResourcePair {
        acquire: &["TCPSocket.new", "UDPSocket.new"],
        release: &[".close"],
        exclude_acquire: &[],
        resource_name: "socket",
    },
    ResourcePair {
        acquire: &[".lock"],
        release: &[".unlock"],
        exclude_acquire: &[],
        resource_name: "mutex",
    },
];

static PHP_RESOURCES: &[ResourcePair] = &[
    ResourcePair {
        acquire: &["fopen"],
        release: &["fclose"],
        exclude_acquire: &["freopen"],
        resource_name: "file handle",
    },
    ResourcePair {
        acquire: &["mysqli_connect"],
        release: &["mysqli_close"],
        exclude_acquire: &[],
        resource_name: "db connection",
    },
    ResourcePair {
        acquire: &["curl_init"],
        release: &["curl_close"],
        exclude_acquire: &[],
        resource_name: "curl handle",
    },
];

static JS_RESOURCES: &[ResourcePair] = &[
    ResourcePair {
        acquire: &["fs.open", "fs.openSync"],
        release: &["fs.close", "fs.closeSync"],
        exclude_acquire: &[],
        resource_name: "file descriptor",
    },
    ResourcePair {
        acquire: &["createReadStream", "createWriteStream"],
        release: &[".close", ".destroy"],
        exclude_acquire: &[],
        resource_name: "stream",
    },
];

pub fn resource_pairs(lang: Lang) -> &'static [ResourcePair] {
    match lang {
        Lang::C => C_RESOURCES,
        Lang::Cpp => C_RESOURCES,
        Lang::Go => GO_RESOURCES,
        Lang::Rust => RUST_RESOURCES,
        Lang::Java => JAVA_RESOURCES,
        Lang::Python => PYTHON_RESOURCES,
        Lang::Ruby => RUBY_RESOURCES,
        Lang::Php => PHP_RESOURCES,
        Lang::JavaScript | Lang::TypeScript => JS_RESOURCES,
    }
}