From e5e4f30cf423fd54c03b2e55dc94dc0285673abb Mon Sep 17 00:00:00 2001 From: Adam Shannon Date: Thu, 4 Jan 2024 17:57:48 -0600 Subject: [PATCH] search/ofac: start parsing out remarks field, map to v2 generalized models --- pkg/ofac/mapper.go | 84 +++++++++++++++++++++++++++++++++++++++++ pkg/ofac/reader.go | 26 ++++++++++++- pkg/ofac/reader_test.go | 45 ++++++++++++++++++++++ pkg/search/models.go | 18 ++++++++- 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 pkg/ofac/mapper.go diff --git a/pkg/ofac/mapper.go b/pkg/ofac/mapper.go new file mode 100644 index 00000000..7c084c9b --- /dev/null +++ b/pkg/ofac/mapper.go @@ -0,0 +1,84 @@ +package ofac + +import ( + "strings" + + "github.com/moov-io/watchman/pkg/search" +) + +func PtrToEntity(sdn *SDN) search.Entity[SDN] { + if sdn != nil { + return ToEntity(*sdn) + } + return search.Entity[SDN]{} +} + +// TODO(adam): Accept Addresses, Alts, Comments + +func ToEntity(sdn SDN) search.Entity[SDN] { + out := search.Entity[SDN]{ + Name: sdn.SDNName, + Source: search.SourceUSOFAC, + SourceData: sdn, + } + + switch strings.ToLower(strings.TrimSpace(sdn.SDNType)) { + case "-0-", "": + out.Type = search.EntityBusiness // TODO(adam): or EntityOrganization + // TODO(adam): How to tell Business vs Organization ? + + case "individual": + out.Type = search.EntityPerson + out.Person = &search.Person{ + Name: sdn.SDNName, + } + + // TODO(adam): + // DOB 02 Aug 1991; + // nationality Russia; + // Gender Male; + // Passport 0291622 (Belize); + + case "vessel": + out.Type = search.EntityVessel + out.Vessel = &search.Vessel{ + Name: sdn.SDNName, + + // IMONumber string `json:"imoNumber"` + // Type VesselType `json:"type"` + // Flag string `json:"flag"` // ISO-3166 + // Built *time.Time `json:"built"` + // Model string `json:"model"` + // Tonnage int `json:"tonnage"` // TODO(adam): remove , and ParseInt + // MMSI string `json:"mmsi"` // Maritime Mobile Service Identity + } + + // TODO(adam): + // Vessel Registration Identification IMO 9569712; + // MMSI 572469210; + // + // Former Vessel Flag None Identified; alt. Former Vessel Flag Tanzania; + + case "aircraft": + out.Type = search.EntityAircraft + out.Aircraft = &search.Aircraft{ + Name: sdn.SDNName, + + // Type AircraftType `json:"type"` + // Flag string `json:"flag"` // ISO-3166 + // Built *time.Time `json:"built"` + // ICAOCode string `json:"icaoCode"` // ICAO aircraft type designator + // Model string `json:"model"` + // SerialNumber string `json:"serialNumber"` + } + + // TODO(adam): + // Aircraft Construction Number (also called L/N or S/N or F/N) 8401; + // Aircraft Manufacture Date 1992; + // Aircraft Model IL76-TD; + // Aircraft Operator YAS AIR; + // Aircraft Manufacturer's Serial Number (MSN) 1023409321; + } + + return out +} diff --git a/pkg/ofac/reader.go b/pkg/ofac/reader.go index e28ade97..1503c2da 100644 --- a/pkg/ofac/reader.go +++ b/pkg/ofac/reader.go @@ -264,6 +264,30 @@ func splitPrograms(in string) []string { return strings.Split(norm, "; ") } +func splitRemarks(input string) []string { + return strings.Split(input, ";") +} + +func findRemarkValues(remarks []string, suffix string) []string { + var out []string + if suffix == "" { + return out + } + for i := range remarks { + idx := strings.Index(remarks[i], suffix) + if idx == -1 { + continue // not found + } + + value := remarks[i][idx+len(suffix):] + value = strings.TrimPrefix(value, ":") // identifiers can end with a colon + value = strings.TrimSuffix(value, ";") + value = strings.TrimSuffix(value, ".") + out = append(out, strings.TrimSpace(value)) + } + return out +} + var ( digitalCurrencies = []string{ "XBT", // Bitcoin @@ -293,7 +317,7 @@ func readDigitalCurrencyAddresses(remarks string) []DigitalCurrencyAddress { // // alt. Digital Currency Address - XBT 12jVCWW1ZhTLA5yVnroEJswqKwsfiZKsax; // - parts := strings.Split(remarks, ";") + parts := splitRemarks(remarks) for i := range parts { // Check if the currency is in the remark var addressIndex int diff --git a/pkg/ofac/reader_test.go b/pkg/ofac/reader_test.go index 35bfc516..abe0c430 100644 --- a/pkg/ofac/reader_test.go +++ b/pkg/ofac/reader_test.go @@ -112,6 +112,51 @@ func TestSDNComments(t *testing.T) { } } +func TestSDN__remarks(t *testing.T) { + // individual + remarks := splitRemarks("DOB 12 Oct 1972; POB Corozal, Belize; Passport 0291622 (Belize); Linked To: D'S SUPERMARKET COMPANY LTD.") + expected := []string{"12 Oct 1972"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "DOB")) + expected = []string{"0291622 (Belize)"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Passport")) + + // Contact info + remarks = splitRemarks("Website www.nitc.co.ir; Email Address info@nitc.co.ir; alt. Email Address administrator@nitc.co.ir; IFCA Determination - Involved in the Shipping Sector; Additional Sanctions Information - Subject to Secondary Sanctions; Telephone (98)(21)(66153220); Telephone (98)(21)(23803202); Telephone (98)(21)(23803303); Telephone (98)(21)(66153224); Telephone (98)(21)(23802230); Telephone (98)(9121115315); Telephone (98)(9128091642); Telephone (98)(9127389031); Fax (98)(21)(22224537); Fax (98)(21)(23803318); Fax (98)(21)(22013392); Fax (98)(21)(22058763).") + expected = []string{"www.nitc.co.ir"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Website")) + expected = []string{"info@nitc.co.ir", "administrator@nitc.co.ir"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Email Address")) + expected = []string{"(98)(21)(66153220)", "(98)(21)(23803202)", "(98)(21)(23803303)", "(98)(21)(66153224)", "(98)(21)(23802230)", "(98)(9121115315)", "(98)(9128091642)", "(98)(9127389031)"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Telephone")) + expected = []string{"(98)(21)(22224537)", "(98)(21)(23803318)", "(98)(21)(22013392)", "(98)(21)(22058763)"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Fax")) + + // Vessel + remarks = splitRemarks("Former Vessel Flag Malta; alt. Former Vessel Flag Tuvalu; alt. Former Vessel Flag None Identified; alt. Former Vessel Flag Tanzania; Additional Sanctions Information - Subject to Secondary Sanctions; Vessel Registration Identification IMO 9187629; MMSI 572469210; Linked To: NATIONAL IRANIAN TANKER COMPANY.") + expected = []string{"9187629"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Vessel Registration Identification IMO")) + expected = []string{"572469210"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "MMSI")) + + // Aircraft + remarks = splitRemarks("Aircraft Construction Number (also called L/N or S/N or F/N) 8401; Aircraft Manufacture Date 1992; Aircraft Model IL76-TD; Aircraft Operator YAS AIR; Aircraft Manufacturer's Serial Number (MSN) 1023409321; Linked To: POUYA AIR.") + expected = []string{"1992"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Manufacture Date")) + expected = []string{"IL76-TD"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Model")) + expected = []string{"(MSN) 1023409321"} + require.ElementsMatch(t, expected, findRemarkValues(remarks, "Serial Number")) + + t.Run("error conditions", func(t *testing.T) { + remarks = splitRemarks("") + require.Len(t, findRemarkValues(remarks, ""), 0) + require.Len(t, findRemarkValues(remarks, "DOB"), 0) + + remarks = splitRemarks(" ; ;;;;; ; ;") + require.Len(t, findRemarkValues(remarks, "DOB"), 0) + }) +} + func TestSDNComments_CryptoCurrencies(t *testing.T) { fd, err := os.CreateTemp("", "sdn-comments") require.NoError(t, err) diff --git a/pkg/search/models.go b/pkg/search/models.go index 3e3171d2..26eb604f 100644 --- a/pkg/search/models.go +++ b/pkg/search/models.go @@ -30,7 +30,7 @@ var ( EntityBusiness EntityType = "business" EntityAircraft EntityType = "aircraft" EntityVessel EntityType = "vessel" - EntityCryptoAddress EntityType = "crypto-address" + EntityCryptoAddress EntityType = "crypto-address" // TODO(adam): Does this make sense? ) type SourceList string @@ -59,8 +59,24 @@ var ( GenderFemale Gender = "female" ) +type ContactInfo struct { + EmailAddresses []string + PhoneNumbers []string + FaxNumbers []string +} + +// TODO(adam): +// +// Website www.tidewaterco.com; +// Email Address info@tidewaterco.com; alt. Email Address info@tidewaterco.ir; +// Telephone: 982188553321; Alt. Telephone: 982188554432; +// Fax: 982188717367; Alt. Fax: 982188708761; +// +// 12803,"TIDEWATER MIDDLE EAST CO.",-0- ,"SDGT] [NPWMD] [IRGC] [IFSR] [IFCA",-0- ,-0- ,-0- ,-0- ,-0- ,-0- ,-0- ," alt. Email Address info@tidewaterco.ir; IFCA Determination - Port Operator; Additional Sanctions Information - Subject to Secondary Sanctions; Business Registration Document # 18745 (Iran); Alt. Fax: 982188708911." + type GovernmentID struct { Type GovernmentIDType `json:"type"` + Country string `json:"country"` // ISO-3166 Identifier string `json:"identifier"` }