nyx-scanner 0.5.0

A multi-language static analysis tool for detecting security vulnerabilities
Documentation
use crate::labels::{Cap, DataLabel, Kind, LabelRule, ParamConfig, RuntimeLabelRule};
use crate::utils::project::{DetectedFramework, FrameworkContext};
use phf::{Map, phf_map};

pub static RULES: &[LabelRule] = &[
    // ─────────── Sources ───────────
    LabelRule {
        matchers: &["os.Getenv"],
        label: DataLabel::Source(Cap::all()),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &[
            "http.Request",
            "r.FormValue",
            "r.URL",
            "r.Body",
            "r.Header",
            "r.URL.Query",
            "r.URL.Query.Get",
            "Request.FormValue",
            "Request.URL",
        ],
        label: DataLabel::Source(Cap::all()),
        case_sensitive: false,
    },
    // ───────── Sanitizers ──────────
    LabelRule {
        matchers: &[
            "html.EscapeString",
            "template.HTMLEscapeString",
            "template.HTMLEscaper",
        ],
        label: DataLabel::Sanitizer(Cap::HTML_ESCAPE),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &["url.QueryEscape", "url.PathEscape"],
        label: DataLabel::Sanitizer(Cap::URL_ENCODE),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &["filepath.Clean", "filepath.Base"],
        label: DataLabel::Sanitizer(Cap::FILE_IO),
        case_sensitive: false,
    },
    // Type conversion sanitizers
    LabelRule {
        matchers: &[
            "strconv.Atoi",
            "strconv.ParseInt",
            "strconv.ParseFloat",
            "strconv.ParseBool",
        ],
        label: DataLabel::Sanitizer(Cap::all()),
        case_sensitive: false,
    },
    // ─────────── Sinks ─────────────
    LabelRule {
        matchers: &["exec.Command"],
        label: DataLabel::Sink(Cap::SHELL_ESCAPE),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &["db.Query", "db.Exec", "db.QueryRow", "db.Prepare"],
        label: DataLabel::Sink(Cap::SQL_QUERY),
        case_sensitive: false,
    },
    // fmt.Printf/Sprintf write to stdout or build strings in memory — not
    // security sinks.  fmt.Fprintf writes to an io.Writer (often http.ResponseWriter)
    // so it IS a security sink for XSS.
    LabelRule {
        matchers: &["fmt.Fprintf"],
        label: DataLabel::Sink(Cap::HTML_ESCAPE),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &[
            "os.Open",
            "os.OpenFile",
            "os.Create",
            "ioutil.ReadFile",
            "os.ReadFile",
            // Mutating filesystem operations.  Path-traversal CVEs commonly
            // sink into delete/write rather than read (Owncast CVE-2024-31450
            // sinks into `os.Remove(filepath.Join(root, userInput))`).
            "os.Remove",
            "os.RemoveAll",
            "os.WriteFile",
            "ioutil.WriteFile",
        ],
        label: DataLabel::Sink(Cap::FILE_IO),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &["template.HTML", "template.JS", "template.CSS"],
        label: DataLabel::Sink(Cap::HTML_ESCAPE),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &[
            "http.Get",
            "http.Post",
            "http.Head",
            "http.NewRequest",
            "http.NewRequestWithContext",
            "net.Dial",
            "net.DialTimeout",
            // `http.DefaultClient` is the package-level default `*http.Client`.
            // Idiomatic Go SSRF sinks (Owncast CVE-2023-3188) use the
            // `http.DefaultClient.Get(url)` form rather than the bare
            // `http.Get(url)` helper, so the suffix-matched callee text needs
            // an explicit entry here — bare `Get/Post/Do/Head` would
            // over-match unrelated method names.
            "http.DefaultClient.Get",
            "http.DefaultClient.Post",
            "http.DefaultClient.Head",
            "http.DefaultClient.Do",
            "http.DefaultClient.PostForm",
        ],
        label: DataLabel::Sink(Cap::SSRF),
        case_sensitive: false,
    },
    LabelRule {
        matchers: &[
            "md5.New",
            "md5.Sum",
            "sha1.New",
            "sha1.Sum",
            "des.NewCipher",
            "rc4.NewCipher",
        ],
        label: DataLabel::Sink(Cap::CRYPTO),
        case_sensitive: false,
    },
];

pub static KINDS: Map<&'static str, Kind> = phf_map! {
    // control-flow
    "if_statement"             => Kind::If,
    "for_statement"            => Kind::For,

    "return_statement"         => Kind::Return,
    "break_statement"          => Kind::Break,
    "continue_statement"       => Kind::Continue,

    // structure
    "source_file"              => Kind::SourceFile,
    "block"                    => Kind::Block,
    "statement_list"           => Kind::Block,
    "function_declaration"     => Kind::Function,
    "method_declaration"       => Kind::Function,
    "func_literal"             => Kind::Function,
    "expression_switch_statement"  => Kind::Switch,
    "type_switch_statement"        => Kind::Switch,
    "expression_case"              => Kind::Block,
    "type_case"                    => Kind::Block,
    "default_case"                 => Kind::Block,
    "select_statement"             => Kind::Block,
    "communication_case"           => Kind::Block,
    "go_statement"                 => Kind::Block,
    "defer_statement"              => Kind::Block,

    // data-flow
    "call_expression"          => Kind::CallFn,
    "assignment_statement"     => Kind::Assignment,
    "short_var_declaration"    => Kind::CallWrapper,
    "expression_statement"     => Kind::CallWrapper,
    "var_declaration"          => Kind::CallWrapper,
    "type_assertion_expression" => Kind::Seq,

    // trivia
    "comment"                  => Kind::Trivia,
    ";"  => Kind::Trivia, ","  => Kind::Trivia,
    "("  => Kind::Trivia, ")"  => Kind::Trivia,
    "{"  => Kind::Trivia, "}"  => Kind::Trivia,
    "\n" => Kind::Trivia,
    "import_declaration"       => Kind::Trivia,
    "package_clause"           => Kind::Trivia,
};

pub static PARAM_CONFIG: ParamConfig = ParamConfig {
    params_field: "parameters",
    param_node_kinds: &["parameter_declaration"],
    self_param_kinds: &[],
    ident_fields: &["name"],
};

/// Framework-conditional rules for Go.
pub fn framework_rules(ctx: &FrameworkContext) -> Vec<RuntimeLabelRule> {
    let mut rules = Vec::new();

    if ctx.has(DetectedFramework::Gin) {
        rules.push(RuntimeLabelRule {
            matchers: vec![
                "c.Param".into(),
                "c.Query".into(),
                "c.PostForm".into(),
                "c.DefaultQuery".into(),
                "c.DefaultPostForm".into(),
                "c.GetHeader".into(),
                "c.Cookie".into(),
                "c.BindJSON".into(),
                "c.ShouldBindJSON".into(),
            ],
            label: DataLabel::Source(Cap::all()),
            case_sensitive: false,
        });
        rules.push(RuntimeLabelRule {
            matchers: vec!["c.HTML".into(), "c.String".into()],
            label: DataLabel::Sink(Cap::HTML_ESCAPE),
            case_sensitive: false,
        });
    }

    if ctx.has(DetectedFramework::Echo) {
        rules.push(RuntimeLabelRule {
            matchers: vec![
                "c.QueryParam".into(),
                "c.FormValue".into(),
                "c.Param".into(),
                "c.Bind".into(),
            ],
            label: DataLabel::Source(Cap::all()),
            case_sensitive: false,
        });
        rules.push(RuntimeLabelRule {
            matchers: vec!["c.HTML".into(), "c.String".into(), "c.JSON".into()],
            label: DataLabel::Sink(Cap::HTML_ESCAPE),
            case_sensitive: false,
        });
    }

    rules
}