nyx-scanner 0.5.0

A multi-language static analysis tool for detecting security vulnerabilities
Documentation
use super::AuthExtractor;
use super::common::{
    attach_route_handler, call_site_from_node, collect_top_level_units, http_method_from_name,
    is_handler_reference, member_target, named_children, push_route_registration,
    string_literal_value, visit_named_nodes,
};
use crate::auth_analysis::config::AuthAnalysisRules;
use crate::auth_analysis::model::{AuthorizationModel, Framework};
use crate::utils::project::{DetectedFramework, FrameworkContext};
use std::path::Path;
use tree_sitter::{Node, Tree};

pub struct ExpressExtractor;

impl AuthExtractor for ExpressExtractor {
    fn supports(&self, lang: &str, framework_ctx: Option<&FrameworkContext>) -> bool {
        matches!(lang, "javascript" | "typescript")
            && framework_ctx
                .is_none_or(|ctx| ctx.frameworks.is_empty() || ctx.has(DetectedFramework::Express))
    }

    fn extract(
        &self,
        tree: &Tree,
        bytes: &[u8],
        path: &Path,
        rules: &AuthAnalysisRules,
    ) -> AuthorizationModel {
        let root = tree.root_node();
        let mut model = AuthorizationModel::default();

        collect_top_level_units(root, bytes, rules, &mut model);
        visit_named_nodes(root, &mut |node| {
            if node.kind() == "call_expression" {
                maybe_collect_route(root, node, bytes, path, rules, &mut model);
            }
        });

        model
    }
}

fn maybe_collect_route(
    root: Node<'_>,
    node: Node<'_>,
    bytes: &[u8],
    path: &Path,
    rules: &AuthAnalysisRules,
    model: &mut AuthorizationModel,
) {
    let Some(function) = node.child_by_field_name("function") else {
        return;
    };
    let Some((object_name, method_name)) = member_target(function, bytes) else {
        return;
    };
    let Some(method) = http_method_from_name(&method_name) else {
        return;
    };
    if !matches!(object_name.as_str(), "router" | "app") {
        return;
    }

    let Some(arguments) = node.child_by_field_name("arguments") else {
        return;
    };
    let named_args = named_children(arguments);
    let Some(path_node) = named_args.first().copied() else {
        return;
    };
    let Some(route_path) = string_literal_value(path_node, bytes) else {
        return;
    };

    let Some((handler_idx, handler_expr)) = named_args
        .iter()
        .enumerate()
        .rev()
        .find(|(_, arg)| is_handler_reference(**arg))
    else {
        return;
    };

    let Some(handler) = attach_route_handler(
        root,
        *handler_expr,
        format!("{:?} {}", method, route_path),
        bytes,
        rules,
        model,
    ) else {
        return;
    };

    let middleware_calls = named_args[1..handler_idx]
        .iter()
        .map(|middleware| call_site_from_node(*middleware, bytes))
        .collect();

    push_route_registration(
        model,
        Framework::Express,
        method,
        route_path,
        path,
        handler,
        middleware_calls,
    );
}