bonsai-ninja-security 0.1.0

Security rulepack loader, matcher, and source/sink/sanitizer wrapper for bonsai-ninja.
Documentation
use super::*;

fn span(file: u32, start: u64, end: u64) -> Span {
    Span::new(bonsai_common::FileId::new(file), start, end)
}

#[test]
fn unresolved_calls_only_mark_terminal_expression_incomplete() {
    let terminal = span(1, 100, 140);

    assert!(unresolved_call_site_is_in_terminal_expression(
        terminal,
        span(1, 112, 128)
    ));
    assert!(unresolved_call_site_is_in_terminal_expression(
        terminal,
        span(1, 100, 140)
    ));
    assert!(!unresolved_call_site_is_in_terminal_expression(
        terminal,
        span(1, 150, 170)
    ));
    assert!(!unresolved_call_site_is_in_terminal_expression(
        terminal,
        span(2, 112, 128)
    ));
    assert!(!unresolved_call_site_is_in_terminal_expression(
        terminal,
        span(1, 90, 150)
    ));
}

#[test]
fn compiler_resolution_gaps_drive_finding_completeness_without_name_guesses() {
    let ws = Workspace::new(bonsai_adapters::all_languages_registry());
    let file = ws.vfs().write(
        "app.py",
        Arc::<str>::from(concat!(
            "def pick(value: str):\n",
            "    return value\n\n",
            "def pick(value: bytes):\n",
            "    return value\n\n",
            "def entry(value):\n",
            "    return sink(pick(value))\n",
        )),
    );
    let _ = ws.db().decl_index(file);
    let call_graph = ws.cached_resolved_call_graph();
    let unresolved = call_graph.unresolved_workspace_call_sites().collect::<Vec<_>>();
    assert_eq!(
        unresolved.len(),
        1,
        "the compiler should report the ambiguous in-workspace overload once"
    );
    let (caller, unresolved_span) = unresolved[0];

    let coverage = ResolutionCoverage::from_graph(call_graph.as_ref(), [caller]);
    assert_eq!(
        coverage.unresolved_workspace_sites,
        AHashSet::from_iter([(caller, unresolved_span)])
    );

    let terminal_span = Span::new(
        unresolved_span.file,
        unresolved_span.start.saturating_sub(1),
        unresolved_span.end.saturating_add(1),
    );
    let graph = EntryTaintGraph {
        tainted_calls: vec![
            TaintedCall {
                parent_trace_id: None,
                caller,
                name: "pick".to_string(),
                call_span: unresolved_span,
                tainted_args: Vec::new(),
                tainted_receiver: None,
                tainted_receiver_source_names: Vec::new(),
                kind: bonsai_taint::TaintedCallKind::Call,
            },
            TaintedCall {
                parent_trace_id: None,
                caller,
                name: "sink".to_string(),
                call_span: terminal_span,
                tainted_args: Vec::new(),
                tainted_receiver: None,
                tainted_receiver_source_names: Vec::new(),
                kind: bonsai_taint::TaintedCallKind::Call,
            },
        ],
        ..EntryTaintGraph::default()
    };
    let index = GraphUnresolvedCallIndex::new(call_graph.as_ref(), &graph);
    assert_eq!(
        index.reasons_for_terminal_call(&graph.tainted_calls[1]),
        vec!["unresolved-call:pick"]
    );
}

#[test]
fn grouped_findings_preserve_incomplete_member_reasons() {
    let mut complete = true;
    let mut reasons = Vec::new();

    merge_analysis_completeness(
        &mut complete,
        &mut reasons,
        false,
        vec!["unresolved-call:encode".to_string()],
    );

    assert!(!complete);
    assert_eq!(reasons, vec!["unresolved-call:encode"]);

    merge_analysis_completeness(
        &mut complete,
        &mut reasons,
        false,
        vec![
            "unresolved-call:encode".to_string(),
            "lineage incomplete".to_string(),
        ],
    );

    assert!(!complete);
    assert_eq!(reasons, vec!["lineage incomplete", "unresolved-call:encode"]);
}