From 028add0703e1b2d571078a8bc04b411e068da633 Mon Sep 17 00:00:00 2001 From: Lacikika <114676193+Lacikika@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:34:25 +0000 Subject: [PATCH 1/2] feat: forensic ballistics, barrel rifling & NIBIN integration Add migration 067 for barrel rifling profiles and NIBIN database. Implement pure Lua deterministic formulas for rifling microstriations, firing pin impressions, shell casing ejection marks, and NIBIN match correlation. Integrate server-side discharge telemetry, NIBIN cross-referencing, and Python test suite. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- ...llistics_rifling_and_nibin_integration.sql | 49 ++++ FiveM/nyx_core/sql/migrations/index.json | 3 +- FiveM/nyx_judiciary/config.lua | 1 + FiveM/nyx_judiciary/fxmanifest.lua | 4 +- FiveM/nyx_judiciary/server/ballistics.lua | 238 ++++++++++++++++++ FiveM/nyx_judiciary/server/exports.lua | 17 ++ FiveM/nyx_judiciary/server/main.lua | 29 ++- FiveM/nyx_judiciary/shared/ballistics.lua | 190 ++++++++++++++ ...llistics_rifling_and_nibin_integration.sql | 49 ++++ ...allistics_rifling_and_nibin_integration.py | 153 +++++++++++ 10 files changed, 729 insertions(+), 4 deletions(-) create mode 100644 FiveM/nyx_core/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql create mode 100644 FiveM/nyx_judiciary/server/ballistics.lua create mode 100644 FiveM/nyx_judiciary/shared/ballistics.lua create mode 100644 FiveM/nyx_judiciary/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql create mode 100644 tests/test_forensic_ballistics_rifling_and_nibin_integration.py diff --git a/FiveM/nyx_core/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql b/FiveM/nyx_core/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql new file mode 100644 index 0000000..53ce1b7 --- /dev/null +++ b/FiveM/nyx_core/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql @@ -0,0 +1,49 @@ +-- ============================================================================ +-- Nyxframework: nyx_judiciary & nyx_mdt +-- Migration: 067_forensic_ballistics_rifling_and_nibin_integration.sql +-- Description: Forensic ballistics engine, barrel rifling microstriation profiles, +-- ejected shell casing firing pin / breech face signature hashing, +-- and National Integrated Ballistic Information Network (NIBIN) database. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create weapon barrel rifling and mechanical signature profiles table +CREATE TABLE IF NOT EXISTS `nyx_ballistics_rifling_profiles` ( + `weapon_serial` VARCHAR(64) NOT NULL PRIMARY KEY, + `weapon_model` VARCHAR(64) NOT NULL, + `caliber` VARCHAR(32) NOT NULL, + `rifling_twist` VARCHAR(32) NOT NULL DEFAULT '1:10_RIGHT', + `groove_count` INT NOT NULL DEFAULT 6, + `breech_face_hash` VARCHAR(64) NOT NULL, + `firing_pin_hash` VARCHAR(64) NOT NULL, + `rifling_microstriation_hash` VARCHAR(64) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_ballistics_model` (`weapon_model`), + INDEX `idx_ballistics_caliber` (`caliber`), + INDEX `idx_ballistics_rifling_hash` (`rifling_microstriation_hash`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create National Integrated Ballistic Information Network (NIBIN) correlation database +CREATE TABLE IF NOT EXISTS `nyx_nibin_database` ( + `entry_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `evidence_id` VARCHAR(64) NOT NULL, + `weapon_serial` VARCHAR(64) NULL, + `casing_ejection_hash` VARCHAR(64) NOT NULL, + `rifling_hash` VARCHAR(64) NOT NULL, + `caliber` VARCHAR(32) NOT NULL, + `crime_scene_docket_id` VARCHAR(64) NULL, + `registered_by_citizen_id` VARCHAR(64) NOT NULL, + `match_status` ENUM('UNMATCHED', 'CANDIDATE_MATCH', 'CONFIRMED_MATCH') NOT NULL DEFAULT 'UNMATCHED', + `matched_entry_id` VARCHAR(64) NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_nibin_evidence` (`evidence_id`), + INDEX `idx_nibin_serial` (`weapon_serial`), + INDEX `idx_nibin_casing_hash` (`casing_ejection_hash`), + INDEX `idx_nibin_rifling_hash` (`rifling_hash`), + INDEX `idx_nibin_status` (`match_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_core/sql/migrations/index.json b/FiveM/nyx_core/sql/migrations/index.json index e5081f1..f7a814c 100644 --- a/FiveM/nyx_core/sql/migrations/index.json +++ b/FiveM/nyx_core/sql/migrations/index.json @@ -64,5 +64,6 @@ "063_pharmaceutical_refining_and_player_chemical_labs.sql", "064_industrial_refining_and_functional_parts_manufacturing.sql", "065_judicial_bail_bonds_surety_escrow_and_fugitive_recovery.sql", - "066_municipal_toll_booths_and_commercial_weigh_stations.sql" + "066_municipal_toll_booths_and_commercial_weigh_stations.sql", + "067_forensic_ballistics_rifling_and_nibin_integration.sql" ] diff --git a/FiveM/nyx_judiciary/config.lua b/FiveM/nyx_judiciary/config.lua index 1d6d6ef..7f5be83 100644 --- a/FiveM/nyx_judiciary/config.lua +++ b/FiveM/nyx_judiciary/config.lua @@ -119,6 +119,7 @@ Config.Forensics = { -- Certified Laboratory Analytical Tests LabAnalysisTypes = { ['ballistics_match'] = { label = 'Ballistics Striation & Rifling Comparison', fee = 250.00 }, + ['nibin_crossref'] = { label = 'National Integrated Ballistic Information Network (NIBIN) Cross-Reference', fee = 300.00 }, ['dna_profile'] = { label = 'Short Tandem Repeat (STR) DNA Profile', fee = 400.00 }, ['gsr_spectrometry'] = { label = 'Scanning Electron Microscopy (GSR-SEM)', fee = 150.00 }, ['fingerprint_afis'] = { label = 'Automated Fingerprint Identification (AFIS)', fee = 200.00 }, diff --git a/FiveM/nyx_judiciary/fxmanifest.lua b/FiveM/nyx_judiciary/fxmanifest.lua index 089a463..68a4e5c 100644 --- a/FiveM/nyx_judiciary/fxmanifest.lua +++ b/FiveM/nyx_judiciary/fxmanifest.lua @@ -19,12 +19,14 @@ shared_scripts { 'shared/courtroom_ui.lua', 'shared/penal_labor.lua', 'shared/acid_etch.lua', - 'shared/bail_bonds.lua' + 'shared/bail_bonds.lua', + 'shared/ballistics.lua' } server_scripts { '@oxmysql/lib/MySQL.lua', 'server/evidence.lua', + 'server/ballistics.lua', 'server/dockets.lua', 'server/warrants.lua', 'server/jury.lua', diff --git a/FiveM/nyx_judiciary/server/ballistics.lua b/FiveM/nyx_judiciary/server/ballistics.lua new file mode 100644 index 0000000..86aa517 --- /dev/null +++ b/FiveM/nyx_judiciary/server/ballistics.lua @@ -0,0 +1,238 @@ +--[[ + Nyxframework: nyx_judiciary + File: server/ballistics.lua + Description: Authoritative server-side forensic ballistics engine, rifling profile registration, + ejected shell casing telemetry tracking, NIBIN automated cross-referencing & case linking. +]] + +Nyx = Nyx or {} +Nyx.Judiciary = Nyx.Judiciary or {} +Nyx.Judiciary.Ballistics = Nyx.Judiciary.Ballistics or {} + +-- 1. Ensure or generate weapon barrel rifling profile +function Nyx.Judiciary.Ballistics.GetOrCreateRiflingProfile(weaponSerial, weaponModel) + weaponSerial = weaponSerial or "NYX-UNKNOWN" + weaponModel = weaponModel or "weapon_pistol" + + local existing = MySQL.single.await([[ + SELECT `weapon_serial`, `weapon_model`, `caliber`, `rifling_twist`, `groove_count`, + `breech_face_hash`, `firing_pin_hash`, `rifling_microstriation_hash` + FROM `nyx_ballistics_rifling_profiles` + WHERE `weapon_serial` = ? + ]], { weaponSerial }) + + if existing then + return existing + end + + local caliber = Nyx.Ballistics.GetCaliber(weaponModel) + local salt = Nyx.Judiciary.GenerateSalt and Nyx.Judiciary.GenerateSalt(8) or "RIFLE_SALT_99" + local breechSalt = Nyx.Judiciary.GenerateSalt and Nyx.Judiciary.GenerateSalt(8) or "BREECH_SALT_88" + + local riflingHash = Nyx.Ballistics.ComputeRiflingHash(weaponSerial, weaponModel, caliber, salt) + local breechPinHash = Nyx.Ballistics.ComputeBreechPinHash(weaponSerial, "STANDARD_RECTANGULAR", "HEMISPHERICAL_PIN", breechSalt) + local firingPinHash = Nyx.Ballistics.Sha256("PIN:" .. weaponSerial .. ":" .. breechSalt) + + MySQL.prepare.await([[ + INSERT INTO `nyx_ballistics_rifling_profiles` + (`weapon_serial`, `weapon_model`, `caliber`, `rifling_twist`, `groove_count`, + `breech_face_hash`, `firing_pin_hash`, `rifling_microstriation_hash`) + VALUES (?, ?, ?, '1:10_RIGHT', 6, ?, ?, ?) + ]], { + weaponSerial, + weaponModel, + caliber, + breechPinHash, + firingPinHash, + riflingHash + }) + + return { + weapon_serial = weaponSerial, + weapon_model = weaponModel, + caliber = caliber, + rifling_twist = "1:10_RIGHT", + groove_count = 6, + breech_face_hash = breechPinHash, + firing_pin_hash = firingPinHash, + rifling_microstriation_hash = riflingHash + } +end + +-- 2. Enhanced Ballistic Event Registration with Shell Casing Microstriation Ejection +function Nyx.Judiciary.Ballistics.RegisterDischargeEvent(src, weaponHash, coords, ejectionAngleDeg) + local player = Nyx.GetPlayer(src) + if not player then return nil end + + local weaponSerial = player:getMetadata("equippedWeaponSerial") or "NYX-UNKNOWN" + local weaponModel = tostring(weaponHash) + local profile = Nyx.Judiciary.Ballistics.GetOrCreateRiflingProfile(weaponSerial, weaponModel) + + ejectionAngleDeg = tonumber(ejectionAngleDeg) or 45.0 + local ejectSalt = Nyx.Judiciary.GenerateSalt and Nyx.Judiciary.GenerateSalt(8) or "EJECT_SALT_77" + local casingEjectionHash = Nyx.Ballistics.ComputeCasingEjectionHash(weaponSerial, profile.breech_face_hash, ejectionAngleDeg, ejectSalt) + + -- Register transient crime scene evidence via main evidence engine + local dropId, rootHash = Nyx.Judiciary.RegisterBallisticEvent(src, weaponHash, coords) + + -- Attach enhanced ballistics metadata to active evidence drop + if dropId and Nyx.Judiciary.ActiveEvidence and Nyx.Judiciary.ActiveEvidence[dropId] then + local active = Nyx.Judiciary.ActiveEvidence[dropId] + active.metadata.casingEjectionHash = casingEjectionHash + active.metadata.riflingHash = profile.rifling_microstriation_hash + active.metadata.breechFaceHash = profile.breech_face_hash + active.metadata.firingPinHash = profile.firing_pin_hash + active.metadata.caliber = profile.caliber + active.metadata.weaponSerial = weaponSerial + active.metadata.ejectionAngle = ejectionAngleDeg + end + + return dropId, rootHash, casingEjectionHash, profile.rifling_microstriation_hash +end + +-- 3. Register Evidence Record in NIBIN Database & Automated Cross-Referencing +function Nyx.Judiciary.Ballistics.RegisterNIBINEntry(src, evidenceId, crimeSceneDocketId) + local officer = Nyx.GetPlayer(src) + if not officer then return false, "OFFICER_NOT_FOUND" end + + local evidence = MySQL.single.await([[ + SELECT `evidence_id`, `type`, `metadata` FROM `nyx_judiciary_evidence` WHERE `evidence_id` = ? + ]], { evidenceId }) + + if not evidence then + return false, "EVIDENCE_NOT_FOUND" + end + + local meta = {} + if type(evidence.metadata) == "string" then + local ok, parsed = pcall(json.decode, evidence.metadata) + if ok and type(parsed) == "table" then meta = parsed end + elseif type(evidence.metadata) == "table" then + meta = evidence.metadata + end + + local casingHash = meta.casingEjectionHash or "CASING_HASH_NONE" + local riflingHash = meta.riflingHash or "RIFLING_HASH_NONE" + local caliber = meta.caliber or "9mm" + local weaponSerial = meta.weaponSerial or nil + + local entryId = "NIBIN-" .. (Nyx.Judiciary.GenerateUUID and Nyx.Judiciary.GenerateUUID() or tostring(os.time())) + + -- Perform automated scan against existing NIBIN entries for matches + local existingEntries = MySQL.query.await([[ + SELECT `entry_id`, `evidence_id`, `weapon_serial`, `casing_ejection_hash`, `rifling_hash`, `caliber`, `match_status` + FROM `nyx_nibin_database` + WHERE `caliber` = ? AND `entry_id` != ? + ]], { caliber, entryId }) + + local bestMatchStatus = "UNMATCHED" + local bestMatchedEntryId = nil + local currentEntryObj = { + weaponSerial = weaponSerial, + casingEjectionHash = casingHash, + riflingHash = riflingHash, + caliber = caliber + } + + for _, existing in ipairs(existingEntries or {}) do + local targetObj = { + weaponSerial = existing.weapon_serial, + casingEjectionHash = existing.casing_ejection_hash, + riflingHash = existing.rifling_hash, + caliber = existing.caliber + } + + local status, score = Nyx.Ballistics.EvaluateNIBINMatch(currentEntryObj, targetObj) + if status == "CONFIRMED_MATCH" then + bestMatchStatus = "CONFIRMED_MATCH" + bestMatchedEntryId = existing.entry_id + + -- Update candidate entry as well + MySQL.prepare.await([[ + UPDATE `nyx_nibin_database` + SET `match_status` = 'CONFIRMED_MATCH', `matched_entry_id` = ? + WHERE `entry_id` = ? + ]], { entryId, existing.entry_id }) + break + elseif status == "CANDIDATE_MATCH" and bestMatchStatus ~= "CONFIRMED_MATCH" then + bestMatchStatus = "CANDIDATE_MATCH" + bestMatchedEntryId = existing.entry_id + end + end + + -- Insert NIBIN Record + local insertOk = MySQL.prepare.await([[ + INSERT INTO `nyx_nibin_database` + (`entry_id`, `evidence_id`, `weapon_serial`, `casing_ejection_hash`, `rifling_hash`, + `caliber`, `crime_scene_docket_id`, `registered_by_citizen_id`, `match_status`, `matched_entry_id`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ]], { + entryId, + evidenceId, + weaponSerial or nil, + casingHash, + riflingHash, + caliber, + crimeSceneDocketId or nil, + officer.citizenId, + bestMatchStatus, + bestMatchedEntryId or nil + }) + + if not insertOk then + return false, "DATABASE_ERROR" + end + + -- Log NIBIN audit block in Chain of Custody + Nyx.Judiciary.AppendCustodyBlock( + evidenceId, + "LAB_ANALYZED", + officer.citizenId, + nil, + "nibin:national_registry", + "NIBIN Correlation Analysis Complete. Status: " .. bestMatchStatus, + { nibinEntryId = entryId, matchStatus = bestMatchStatus, matchedEntryId = bestMatchedEntryId } + ) + + return true, { + entryId = entryId, + evidenceId = evidenceId, + matchStatus = bestMatchStatus, + matchedEntryId = bestMatchedEntryId, + casingEjectionHash = casingHash, + riflingHash = riflingHash + } +end + +-- 4. Query NIBIN Correlation Records for MDT / Forensic UI +function Nyx.Judiciary.Ballistics.QueryNIBINRecords(filterCaliber, matchStatusOnly) + local conditions = {} + local params = {} + + if filterCaliber and filterCaliber ~= "all" and filterCaliber ~= "" then + table.insert(conditions, "`caliber` = ?") + table.insert(params, filterCaliber) + end + + if matchStatusOnly and matchStatusOnly ~= "all" and matchStatusOnly ~= "" then + table.insert(conditions, "`match_status` = ?") + table.insert(params, matchStatusOnly) + end + + local whereClause = "" + if #conditions > 0 then + whereClause = "WHERE " .. table.concat(conditions, " AND ") + end + + local sql = string.format([[ + SELECT `entry_id`, `evidence_id`, `weapon_serial`, `casing_ejection_hash`, `rifling_hash`, + `caliber`, `crime_scene_docket_id`, `registered_by_citizen_id`, `match_status`, + `matched_entry_id`, `created_at` + FROM `nyx_nibin_database` + %s + ORDER BY `created_at` DESC + LIMIT 50 + ]], whereClause) + + return MySQL.query.await(sql, params) or {} +end diff --git a/FiveM/nyx_judiciary/server/exports.lua b/FiveM/nyx_judiciary/server/exports.lua index e96f958..52db598 100644 --- a/FiveM/nyx_judiciary/server/exports.lua +++ b/FiveM/nyx_judiciary/server/exports.lua @@ -263,3 +263,20 @@ end) exports("ForfeitBailBond", function(docketId, reason) return Nyx.Judiciary.ForfeitBail(docketId, reason) end) + +-- Forensic Ballistics & NIBIN Integration Exports +exports("GetOrCreateRiflingProfile", function(weaponSerial, weaponModel) + return Nyx.Judiciary.Ballistics.GetOrCreateRiflingProfile(weaponSerial, weaponModel) +end) + +exports("RegisterDischargeEvent", function(src, weaponHash, coords, ejectionAngleDeg) + return Nyx.Judiciary.Ballistics.RegisterDischargeEvent(src, weaponHash, coords, ejectionAngleDeg) +end) + +exports("RegisterNIBINEntry", function(src, evidenceId, crimeSceneDocketId) + return Nyx.Judiciary.Ballistics.RegisterNIBINEntry(src, evidenceId, crimeSceneDocketId) +end) + +exports("QueryNIBINRecords", function(filterCaliber, matchStatusOnly) + return Nyx.Judiciary.Ballistics.QueryNIBINRecords(filterCaliber, matchStatusOnly) +end) diff --git a/FiveM/nyx_judiciary/server/main.lua b/FiveM/nyx_judiciary/server/main.lua index 3a59920..91a0226 100644 --- a/FiveM/nyx_judiciary/server/main.lua +++ b/FiveM/nyx_judiciary/server/main.lua @@ -591,6 +591,27 @@ CreateThread(function() local ok, res = Nyx.Judiciary.ConcludeTrial(docketId, verdict) return { success = ok, result = res, error = (not ok and res or nil) } end) + + -- Forensic Ballistics & NIBIN Integration RPCs + exports['nyx_core']:RegisterRPC("judiciary:registerNIBINEntry", function(src, payload) + if not payload or not payload.evidenceId then + return { success = false, error = "INVALID_PAYLOAD" } + end + local ok, res = Nyx.Judiciary.Ballistics.RegisterNIBINEntry(src, payload.evidenceId, payload.docketId) + return { success = ok, result = res, error = (not ok and res or nil) } + end) + + exports['nyx_core']:RegisterRPC("judiciary:queryNIBINRecords", function(src, payload) + payload = payload or {} + local rows = Nyx.Judiciary.Ballistics.QueryNIBINRecords(payload.caliber, payload.matchStatus) + return { success = true, records = rows } + end) + + exports['nyx_core']:RegisterRPC("judiciary:getRiflingProfile", function(src, weaponSerial) + if not weaponSerial then return { success = false, error = "INVALID_SERIAL" } end + local profile = Nyx.Judiciary.Ballistics.GetOrCreateRiflingProfile(weaponSerial, "weapon_pistol") + return { success = true, profile = profile } + end) end -- 2. Evidence Sweeper Background Task (every 60s) @@ -705,10 +726,14 @@ end) -- Network Event Bridges for Client Action Hooks -RegisterNetEvent("nyx:judiciary:server:weaponFired", function(weaponHash, coords) +RegisterNetEvent("nyx:judiciary:server:weaponFired", function(weaponHash, coords, ejectionAngleDeg) local src = source if not coords or type(coords) ~= "vector3" and type(coords) ~= "table" then return end - Nyx.Judiciary.RegisterBallisticEvent(src, weaponHash, coords) + if Nyx.Judiciary.Ballistics and Nyx.Judiciary.Ballistics.RegisterDischargeEvent then + Nyx.Judiciary.Ballistics.RegisterDischargeEvent(src, weaponHash, coords, ejectionAngleDeg) + else + Nyx.Judiciary.RegisterBallisticEvent(src, weaponHash, coords) + end end) RegisterNetEvent("nyx:judiciary:server:traumaOccurred", function(victimSrc, damageType, coords) diff --git a/FiveM/nyx_judiciary/shared/ballistics.lua b/FiveM/nyx_judiciary/shared/ballistics.lua new file mode 100644 index 0000000..35c1cca --- /dev/null +++ b/FiveM/nyx_judiciary/shared/ballistics.lua @@ -0,0 +1,190 @@ +--[[ + Nyxframework: nyx_judiciary + File: shared/ballistics.lua + Description: Deterministic forensic ballistics engine, barrel rifling microstriation formulas, + firing pin & breech face signature hashing, and NIBIN match correlation algorithms. +]] + +Nyx = Nyx or {} +Nyx.Ballistics = Nyx.Ballistics or {} + +-- Pure Lua SHA-256 Hasher +local function sha256(str) + local band, bor, bxor, bnot = function(a, b) return a & b end, function(a, b) return a | b end, function(a, b) return a ~ b end, function(a) return ~a & 0xFFFFFFFF end + local rshift, lshift = function(a, b) return (a & 0xFFFFFFFF) >> b end, function(a, b) return (a << b) & 0xFFFFFFFF end + + local function ror(x, n) + return (rshift(x, n) | lshift(x, 32 - n)) & 0xFFFFFFFF + end + + local K = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + } + + local H = { + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + } + + local len = #str + local bit_len = len * 8 + local pad = { str:byte(1, len) } + pad[#pad + 1] = 0x80 + while (#pad % 64) ~= 56 do + pad[#pad + 1] = 0x00 + end + + for i = 7, 0, -1 do + pad[#pad + 1] = (bit_len >> (i * 8)) & 0xFF + end + + local W = {} + for chunk = 1, #pad, 64 do + for t = 0, 15 do + local idx = chunk + t * 4 + W[t] = (lshift(pad[idx], 24) | lshift(pad[idx + 1], 16) | lshift(pad[idx + 2], 8) | pad[idx + 3]) & 0xFFFFFFFF + end + for t = 16, 63 do + local s0 = bxor(bxor(ror(W[t - 15], 7), ror(W[t - 15], 18)), rshift(W[t - 15], 3)) + local s1 = bxor(bxor(ror(W[t - 2], 17), ror(W[t - 2], 19)), rshift(W[t - 2], 10)) + W[t] = (W[t - 16] + s0 + W[t - 7] + s1) & 0xFFFFFFFF + end + + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + + for t = 0, 63 do + local S1 = bxor(bxor(ror(e, 6), ror(e, 11)), ror(e, 25)) + local ch = bxor(band(e, f), band(bnot(e), g)) + local temp1 = (h + S1 + ch + K[t + 1] + W[t]) & 0xFFFFFFFF + local S0 = bxor(bxor(ror(a, 2), ror(a, 13)), ror(a, 22)) + local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c)) + local temp2 = (S0 + maj) & 0xFFFFFFFF + + h = g + g = f + f = e + e = (d + temp1) & 0xFFFFFFFF + d = c + c = b + b = a + a = (temp1 + temp2) & 0xFFFFFFFF + end + + H[1] = (H[1] + a) & 0xFFFFFFFF + H[2] = (H[2] + b) & 0xFFFFFFFF + H[3] = (H[3] + c) & 0xFFFFFFFF + H[4] = (H[4] + d) & 0xFFFFFFFF + H[5] = (H[5] + e) & 0xFFFFFFFF + H[6] = (H[6] + f) & 0xFFFFFFFF + H[7] = (H[7] + g) & 0xFFFFFFFF + H[8] = (H[8] + h) & 0xFFFFFFFF + end + + return string.format("%08x%08x%08x%08x%08x%08x%08x%08x", H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8]) +end + +Nyx.Ballistics.Sha256 = sha256 + +-- Helper: Map weapon model / hash to standard caliber string +function Nyx.Ballistics.GetCaliber(weaponModelOrHash) + if not weaponModelOrHash then return "9mm" end + local str = string.lower(tostring(weaponModelOrHash)) + + if str:find("pistol") or str:find("smg") or str:find("combatpistol") or str == tostring(`WEAPON_PISTOL`) or str == tostring(`WEAPON_COMBATPISTOL`) or str == tostring(`WEAPON_SMG`) then + return "9mm" + elseif str:find("appistol") or str:find("vintagepistol") or str:find("snspistol") then + return ".45_ACP" + elseif str:find("carbine") or str:find("rifle") or str:find("specialcarbine") or str == tostring(`WEAPON_CARBINERIFLE`) or str == tostring(`WEAPON_SPECIALCARBINE`) then + return "5.56x45mm" + elseif str:find("mg") or str:find("sniper") or str:find("heavy") then + return "7.62x51mm" + elseif str:find("shotgun") or str:find("pumpshotgun") or str == tostring(`WEAPON_PUMPSHOTGUN`) then + return "12-gauge" + end + + return "9mm" +end + +-- Pure Formula 1: Barrel Rifling Microstriation Signature Hash +-- SHA-256(RIFLING::::) +function Nyx.Ballistics.ComputeRiflingHash(weaponSerial, weaponModel, caliber, salt) + weaponSerial = weaponSerial or "UNKNOWN_SERIAL" + weaponModel = weaponModel or "weapon_pistol" + caliber = caliber or Nyx.Ballistics.GetCaliber(weaponModel) + salt = salt or "RIFLE_SALT_99" + + local raw = string.format("RIFLING:%s:%s:%s:%s", weaponSerial, weaponModel, caliber, salt) + return sha256(raw) +end + +-- Pure Formula 2: Firing Pin & Breech Face Impressions Signature Hash +-- SHA-256(MECHANICAL::::) +function Nyx.Ballistics.ComputeBreechPinHash(weaponSerial, breechFaceType, firingPinType, salt) + weaponSerial = weaponSerial or "UNKNOWN_SERIAL" + breechFaceType = breechFaceType or "STANDARD_RECTANGULAR" + firingPinType = firingPinType or "HEMISPHERICAL_PIN" + salt = salt or "BREECH_SALT_88" + + local raw = string.format("MECHANICAL:%s:%s:%s:%s", weaponSerial, breechFaceType, firingPinType, salt) + return sha256(raw) +end + +-- Pure Formula 3: Shell Casing Ejection Mark Signature Hash +-- SHA-256(CASING_EJECT::::) +function Nyx.Ballistics.ComputeCasingEjectionHash(weaponSerial, breechPinHash, ejectionAngleDeg, salt) + weaponSerial = weaponSerial or "UNKNOWN_SERIAL" + breechPinHash = breechPinHash or "0000000000000000000000000000000000000000000000000000000000000000" + ejectionAngleDeg = tonumber(ejectionAngleDeg) or 45.0 + salt = salt or "EJECT_SALT_77" + + local raw = string.format("CASING_EJECT:%s:%s:%.2f:%s", weaponSerial, breechPinHash, ejectionAngleDeg, salt) + return sha256(raw) +end + +-- Pure Formula 4: NIBIN Correlation Match Logic +-- Evaluates two ballistics records (entryA and entryB) +-- Returns matchStatus ('CONFIRMED_MATCH', 'CANDIDATE_MATCH', 'UNMATCHED') and matchScore (0.0 - 1.0) +function Nyx.Ballistics.EvaluateNIBINMatch(entryA, entryB) + if not entryA or not entryB then + return "UNMATCHED", 0.0 + end + + -- Direct serial match + if entryA.weaponSerial and entryB.weaponSerial and entryA.weaponSerial == entryB.weaponSerial and entryA.weaponSerial ~= "NYX-UNKNOWN" then + return "CONFIRMED_MATCH", 1.0 + end + + -- Rifling microstriation match + if entryA.riflingHash and entryB.riflingHash and entryA.riflingHash == entryB.riflingHash then + return "CONFIRMED_MATCH", 0.98 + end + + -- Casing ejection mark match + if entryA.casingEjectionHash and entryB.casingEjectionHash and entryA.casingEjectionHash == entryB.casingEjectionHash then + return "CONFIRMED_MATCH", 0.95 + end + + -- Caliber check + Breech pin / partial hash similarity (Candidate Match) + if entryA.caliber and entryB.caliber and entryA.caliber == entryB.caliber then + if entryA.casingEjectionHash and entryB.casingEjectionHash then + -- Substring prefix match for partial striations / scratched barrels + if entryA.casingEjectionHash:sub(1, 16) == entryB.casingEjectionHash:sub(1, 16) then + return "CANDIDATE_MATCH", 0.75 + end + end + if entryA.riflingHash and entryB.riflingHash then + if entryA.riflingHash:sub(1, 16) == entryB.riflingHash:sub(1, 16) then + return "CANDIDATE_MATCH", 0.70 + end + end + end + + return "UNMATCHED", 0.0 +end diff --git a/FiveM/nyx_judiciary/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql b/FiveM/nyx_judiciary/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql new file mode 100644 index 0000000..53ce1b7 --- /dev/null +++ b/FiveM/nyx_judiciary/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql @@ -0,0 +1,49 @@ +-- ============================================================================ +-- Nyxframework: nyx_judiciary & nyx_mdt +-- Migration: 067_forensic_ballistics_rifling_and_nibin_integration.sql +-- Description: Forensic ballistics engine, barrel rifling microstriation profiles, +-- ejected shell casing firing pin / breech face signature hashing, +-- and National Integrated Ballistic Information Network (NIBIN) database. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create weapon barrel rifling and mechanical signature profiles table +CREATE TABLE IF NOT EXISTS `nyx_ballistics_rifling_profiles` ( + `weapon_serial` VARCHAR(64) NOT NULL PRIMARY KEY, + `weapon_model` VARCHAR(64) NOT NULL, + `caliber` VARCHAR(32) NOT NULL, + `rifling_twist` VARCHAR(32) NOT NULL DEFAULT '1:10_RIGHT', + `groove_count` INT NOT NULL DEFAULT 6, + `breech_face_hash` VARCHAR(64) NOT NULL, + `firing_pin_hash` VARCHAR(64) NOT NULL, + `rifling_microstriation_hash` VARCHAR(64) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_ballistics_model` (`weapon_model`), + INDEX `idx_ballistics_caliber` (`caliber`), + INDEX `idx_ballistics_rifling_hash` (`rifling_microstriation_hash`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create National Integrated Ballistic Information Network (NIBIN) correlation database +CREATE TABLE IF NOT EXISTS `nyx_nibin_database` ( + `entry_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `evidence_id` VARCHAR(64) NOT NULL, + `weapon_serial` VARCHAR(64) NULL, + `casing_ejection_hash` VARCHAR(64) NOT NULL, + `rifling_hash` VARCHAR(64) NOT NULL, + `caliber` VARCHAR(32) NOT NULL, + `crime_scene_docket_id` VARCHAR(64) NULL, + `registered_by_citizen_id` VARCHAR(64) NOT NULL, + `match_status` ENUM('UNMATCHED', 'CANDIDATE_MATCH', 'CONFIRMED_MATCH') NOT NULL DEFAULT 'UNMATCHED', + `matched_entry_id` VARCHAR(64) NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_nibin_evidence` (`evidence_id`), + INDEX `idx_nibin_serial` (`weapon_serial`), + INDEX `idx_nibin_casing_hash` (`casing_ejection_hash`), + INDEX `idx_nibin_rifling_hash` (`rifling_hash`), + INDEX `idx_nibin_status` (`match_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/tests/test_forensic_ballistics_rifling_and_nibin_integration.py b/tests/test_forensic_ballistics_rifling_and_nibin_integration.py new file mode 100644 index 0000000..15274a3 --- /dev/null +++ b/tests/test_forensic_ballistics_rifling_and_nibin_integration.py @@ -0,0 +1,153 @@ +import pytest +import os +import json +import hashlib + +def sha256(text: str) -> str: + return hashlib.sha256(text.encode('utf-8')).hexdigest() + +def get_caliber(weapon_model_or_hash: str) -> str: + if not weapon_model_or_hash: + return "9mm" + s = str(weapon_model_or_hash).lower() + if "pistol" in s or "smg" in s or "combatpistol" in s or s == "weapon_pistol" or s == "weapon_combatpistol": + return "9mm" + elif "appistol" in s or "vintagepistol" in s or "snspistol" in s: + return ".45_ACP" + elif "carbine" in s or "rifle" in s or "specialcarbine" in s or s == "weapon_carbinerifle": + return "5.56x45mm" + elif "mg" in s or "sniper" in s or "heavy" in s: + return "7.62x51mm" + elif "shotgun" in s or "pumpshotgun" in s: + return "12-gauge" + return "9mm" + +def compute_rifling_hash(weapon_serial: str, weapon_model: str, caliber: str, salt: str) -> str: + raw = f"RIFLING:{weapon_serial}:{weapon_model}:{caliber}:{salt}" + return sha256(raw) + +def compute_breech_pin_hash(weapon_serial: str, breech_face_type: str, firing_pin_type: str, salt: str) -> str: + raw = f"MECHANICAL:{weapon_serial}:{breech_face_type}:{firing_pin_type}:{salt}" + return sha256(raw) + +def compute_casing_ejection_hash(weapon_serial: str, breech_pin_hash: str, ejection_angle: float, salt: str) -> str: + raw = f"CASING_EJECT:{weapon_serial}:{breech_pin_hash}:{ejection_angle:.2f}:{salt}" + return sha256(raw) + +def evaluate_nibin_match(entry_a: dict, entry_b: dict): + if not entry_a or not entry_b: + return "UNMATCHED", 0.0 + + if entry_a.get("weaponSerial") and entry_b.get("weaponSerial") and entry_a["weaponSerial"] == entry_b["weaponSerial"] and entry_a["weaponSerial"] != "NYX-UNKNOWN": + return "CONFIRMED_MATCH", 1.0 + + if entry_a.get("riflingHash") and entry_b.get("riflingHash") and entry_a["riflingHash"] == entry_b["riflingHash"]: + return "CONFIRMED_MATCH", 0.98 + + if entry_a.get("casingEjectionHash") and entry_b.get("casingEjectionHash") and entry_a["casingEjectionHash"] == entry_b["casingEjectionHash"]: + return "CONFIRMED_MATCH", 0.95 + + if entry_a.get("caliber") and entry_b.get("caliber") and entry_a["caliber"] == entry_b["caliber"]: + if entry_a.get("casingEjectionHash") and entry_b.get("casingEjectionHash"): + if entry_a["casingEjectionHash"][:16] == entry_b["casingEjectionHash"][:16]: + return "CANDIDATE_MATCH", 0.75 + if entry_a.get("riflingHash") and entry_b.get("riflingHash"): + if entry_a["riflingHash"][:16] == entry_b["riflingHash"][:16]: + return "CANDIDATE_MATCH", 0.70 + + return "UNMATCHED", 0.0 + + +def test_migration_067_file_and_index(): + core_mig_path = "FiveM/nyx_core/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql" + judiciary_mig_path = "FiveM/nyx_judiciary/sql/migrations/067_forensic_ballistics_rifling_and_nibin_integration.sql" + index_path = "FiveM/nyx_core/sql/migrations/index.json" + + assert os.path.isfile(core_mig_path) + assert os.path.isfile(judiciary_mig_path) + + with open(core_mig_path, "r", encoding="utf-8") as f: + content = f.read() + assert "nyx_ballistics_rifling_profiles" in content + assert "nyx_nibin_database" in content + assert "breech_face_hash" in content + assert "rifling_microstriation_hash" in content + assert "casing_ejection_hash" in content + + with open(index_path, "r", encoding="utf-8") as f: + index_data = json.load(f) + assert "067_forensic_ballistics_rifling_and_nibin_integration.sql" in index_data + + +def test_deterministic_ballistics_formulas(): + serial = "NYX-W99001" + model = "weapon_combatpistol" + cal = get_caliber(model) + assert cal == "9mm" + + salt_r = "SALT_RIFLE_1" + rifling_hash1 = compute_rifling_hash(serial, model, cal, salt_r) + rifling_hash2 = compute_rifling_hash(serial, model, cal, salt_r) + assert rifling_hash1 == rifling_hash2 + assert len(rifling_hash1) == 64 + + breech_pin_hash = compute_breech_pin_hash(serial, "STANDARD_RECTANGULAR", "HEMISPHERICAL_PIN", "SALT_BREECH_1") + assert len(breech_pin_hash) == 64 + + casing_hash1 = compute_casing_ejection_hash(serial, breech_pin_hash, 45.0, "SALT_EJECT_1") + casing_hash2 = compute_casing_ejection_hash(serial, breech_pin_hash, 45.0, "SALT_EJECT_1") + assert casing_hash1 == casing_hash2 + assert len(casing_hash1) == 64 + + +def test_nibin_correlation_logic(): + entry_a = { + "weaponSerial": "NYX-SERIAL-A", + "riflingHash": "a1b2c3d4e5f67890123456789012345678901234567890123456789012345678", + "casingEjectionHash": "1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff", + "caliber": "9mm" + } + + entry_b = { + "weaponSerial": "NYX-SERIAL-A", + "riflingHash": "different_rifling_hash", + "casingEjectionHash": "different_casing_hash", + "caliber": "9mm" + } + + status, score = evaluate_nibin_match(entry_a, entry_b) + assert status == "CONFIRMED_MATCH" + assert score == 1.0 + + entry_c = { + "weaponSerial": "NYX-SERIAL-C", + "riflingHash": "a1b2c3d4e5f67890123456789012345678901234567890123456789012345678", + "casingEjectionHash": "9999888877776666555544443333222211110000ffffeeeeddccbbaa99887766", + "caliber": "9mm" + } + + status, score = evaluate_nibin_match(entry_a, entry_c) + assert status == "CONFIRMED_MATCH" + assert score == 0.98 + + entry_d = { + "weaponSerial": "NYX-SERIAL-D", + "riflingHash": "a1b2c3d4e5f67890_different_tail_hash", + "casingEjectionHash": "1111222233334444_different_tail_casing", + "caliber": "9mm" + } + + status, score = evaluate_nibin_match(entry_a, entry_d) + assert status == "CANDIDATE_MATCH" + assert score == 0.75 + + entry_unmatched = { + "weaponSerial": "NYX-SERIAL-E", + "riflingHash": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + "casingEjectionHash": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", + "caliber": ".45_ACP" + } + + status, score = evaluate_nibin_match(entry_a, entry_unmatched) + assert status == "UNMATCHED" + assert score == 0.0 From e14e0e9ea1db5c94ec56bcb652c8ee174d566378 Mon Sep 17 00:00:00 2001 From: Lacikika <114676193+Lacikika@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:45:13 +0000 Subject: [PATCH 2/2] docs: add AGENTS.md for AI pipeline agent instructions Add AGENTS.md detailing migration detection, collision checks, and test suite execution requirements for autonomous pipeline workers. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- AGENTS.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e6a35a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +# 🤖 Instructions for AI Pipeline Agents + +Welcome to `Lacikika/Nyxframework`. When developing features or fixing bugs in this codebase, adhere strictly to the following instructions. + +--- + +## 📋 Merge Conflict & Schema Collision Check Protocol + +Before submitting a Pull Request, every AI agent **MUST** execute the following checks: + +1. **Database Migration Sequence & State Detection:** + - Inspect `FiveM/nyx_core/sql/migrations/index.json` and directory listings in `FiveM/nyx_core/sql/migrations/`. + - Ensure the new migration N+1 is strictly sequential with 3-digit zero-padding (`067_.sql`). + - Register the new SQL migration file in BOTH `FiveM/nyx_core/sql/migrations/index.json` AND `FiveM/nyx_core/server/migrations.lua` (if fallback manifest exists). + - Mirror the SQL migration across any module migration folders (e.g. `FiveM/nyx_/sql/migrations/`). + +2. **Branch & Git Collision Prevention:** + - Verify active git branches and origin tracking to prevent branch naming collisions or conflicting migration sequence numbers. + +3. **Validation & Test Execution:** + - Execute all verification tools before submission: + ```bash + python3 scripts/validator.py + python3 FiveM/scripts/bootstrap_validator.py + pytest + ``` + - Ensure all 14 resource manifests, Lua scripts, SQL migrations, and test suites pass cleanly with zero errors.