mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
A Go AML engine: a configurable rules DSL over expr-lang with weight-of-evidence scoring, sanctions screening against OFAC/UN/EU/HMT, case management, and scored alerting. Single binary, embedded SQLite per org, embedded UI. Surface (all /v1, org-scoped): /v1/aml/transactions ingest and score /v1/aml/rules /rules/test the rules DSL and its dry-run /v1/aml/sanctions/search list screening /v1/aml/cases investigation workflow /v1/aml/health Build: make build produces one static binary with the UI embedded (embedui tag); bare Go tooling needs no Node. Third-party license and notice texts are generated from the module graph the linker actually walks and ship beside every artifact. Co-authored-by: Hanzo Dev <[email protected]>
100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
package sanctions
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestIngestXMLEntityExpansion (RED-05) verifies that the OFAC XML parser
|
|
// rejects entity expansion attacks (billion laughs / XML bombs). Without
|
|
// the streaming decoder fix, this would cause exponential memory growth.
|
|
func TestIngestXMLEntityExpansion(t *testing.T) {
|
|
// Classic billion-laughs XML bomb with nested entity expansion.
|
|
bomb := `<?xml version="1.0"?>
|
|
<!DOCTYPE lolz [
|
|
<!ENTITY lol "lol">
|
|
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
|
|
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
|
|
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
|
|
]>
|
|
<sdnList>
|
|
<sdnEntry>
|
|
<uid>1</uid>
|
|
<firstName>&lol4;</firstName>
|
|
<lastName>Test</lastName>
|
|
</sdnEntry>
|
|
</sdnList>`
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.Write([]byte(bomb))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ing := NewIngester()
|
|
_, _, err := ing.FetchOFAC(context.Background(), srv.URL)
|
|
if err == nil {
|
|
t.Fatal("expected error parsing XML bomb with entity expansion, got nil")
|
|
}
|
|
// The error should be about entity references, not OOM.
|
|
t.Logf("correctly rejected XML bomb: %v", err)
|
|
}
|
|
|
|
// TestIngestValidOFACXML ensures normal OFAC XML still parses correctly
|
|
// after the streaming decoder change.
|
|
func TestIngestValidOFACXML(t *testing.T) {
|
|
validXML := `<?xml version="1.0"?>
|
|
<sdnList>
|
|
<sdnEntry>
|
|
<uid>1234</uid>
|
|
<firstName>John</firstName>
|
|
<lastName>Doe</lastName>
|
|
<sdnType>Individual</sdnType>
|
|
<akaList>
|
|
<aka>
|
|
<firstName>Johnny</firstName>
|
|
<lastName>Doe</lastName>
|
|
</aka>
|
|
</akaList>
|
|
</sdnEntry>
|
|
<sdnEntry>
|
|
<uid>5678</uid>
|
|
<firstName></firstName>
|
|
<lastName>Evil Corp</lastName>
|
|
<sdnType>Entity</sdnType>
|
|
</sdnEntry>
|
|
</sdnList>`
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.Write([]byte(validXML))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ing := NewIngester()
|
|
entries, hash, err := ing.FetchOFAC(context.Background(), srv.URL)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if hash == "" {
|
|
t.Fatal("expected non-empty hash")
|
|
}
|
|
if len(entries) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(entries))
|
|
}
|
|
if entries[0].Name != "John Doe" {
|
|
t.Errorf("entry[0].Name = %q, want %q", entries[0].Name, "John Doe")
|
|
}
|
|
if entries[0].Type != "individual" {
|
|
t.Errorf("entry[0].Type = %q, want %q", entries[0].Type, "individual")
|
|
}
|
|
if len(entries[0].Aliases) != 1 || entries[0].Aliases[0] != "Johnny Doe" {
|
|
t.Errorf("entry[0].Aliases = %v, want [Johnny Doe]", entries[0].Aliases)
|
|
}
|
|
if entries[1].Type != "entity" {
|
|
t.Errorf("entry[1].Type = %q, want %q", entries[1].Type, "entity")
|
|
}
|
|
}
|