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. 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_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