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_sites_from_value, collect_top_level_units, http_method_from_name,
    is_handler_reference, member_target, named_children, object_property_value,
    push_route_registration, string_literal_value, visit_named_nodes,
};
use crate::auth_analysis::config::AuthAnalysisRules;
use crate::auth_analysis::model::{AuthorizationModel, CallSite, Framework};
use crate::utils::project::{DetectedFramework, FrameworkContext};
use std::path::Path;
use tree_sitter::{Node, Tree};

pub struct FastifyExtractor;

impl AuthExtractor for FastifyExtractor {
    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::Fastify))
    }

    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_shorthand_route(root, node, bytes, path, rules, &mut model);
                maybe_collect_route_object(root, node, bytes, path, rules, &mut model);
            }
        });

        model
    }
}

fn maybe_collect_shorthand_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(), "fastify" | "app" | "server") {
        return;
    }

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

    let options = args.get(1).copied().filter(|node| node.kind() == "object");
    let handler_expr = args
        .last()
        .copied()
        .filter(|node| is_handler_reference(*node))
        .or_else(|| options.and_then(|opts| object_property_value(opts, bytes, &["handler"])));
    let Some(handler_expr) = handler_expr else {
        return;
    };

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

    let middleware_calls = options
        .map(|opts| collect_fastify_hooks(opts, bytes))
        .unwrap_or_default();

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

fn maybe_collect_route_object(
    root: Node<'_>,
    node: Node<'_>,
    bytes: &[u8],
    path: &Path,
    rules: &AuthAnalysisRules,
    model: &mut AuthorizationModel,
) {
    let Some(function) = node.child_by_field_name("function") else {
        return;
    };
    if !is_fastify_route_call(function, bytes) {
        return;
    }

    let Some(arguments) = node.child_by_field_name("arguments") else {
        return;
    };
    let Some(route_object) = named_children(arguments).first().copied() else {
        return;
    };
    if route_object.kind() != "object" {
        return;
    }

    let Some(method_text) = object_property_value(route_object, bytes, &["method"])
        .and_then(|value| string_literal_value(value, bytes))
    else {
        return;
    };
    let Some(method) = http_method_from_name(&method_text) else {
        return;
    };
    let Some(route_path) = object_property_value(route_object, bytes, &["url", "path"])
        .and_then(|value| string_literal_value(value, bytes))
    else {
        return;
    };
    let Some(handler_expr) = object_property_value(route_object, bytes, &["handler"]) else {
        return;
    };
    let Some(handler) = attach_route_handler(
        root,
        handler_expr,
        format!("{:?} {}", method, route_path),
        bytes,
        rules,
        model,
    ) else {
        return;
    };

    let middleware_calls = collect_fastify_hooks(route_object, bytes);

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

fn collect_fastify_hooks(node: Node<'_>, bytes: &[u8]) -> Vec<CallSite> {
    let mut hooks = Vec::new();
    for field in ["preHandler", "preValidation", "onRequest"] {
        if let Some(value) = object_property_value(node, bytes, &[field]) {
            hooks.extend(call_sites_from_value(value, bytes));
        }
    }
    hooks
}

fn is_fastify_route_call(node: Node<'_>, bytes: &[u8]) -> bool {
    member_target(node, bytes).is_some_and(|(object_name, property)| {
        matches!(object_name.as_str(), "fastify" | "app" | "server") && property == "route"
    })
}