1+ // composables/useHashResults.js
2+ // ─────────────────────────────────────────────────────────────────────────────
3+ // All state, computed properties, table config, cell rendering, export/copy,
4+ // and file-upload logic specific to hash scan results.
5+ // Uses Vue 3 GLOBAL build (window.Vue) — no bundler required.
6+ // ─────────────────────────────────────────────────────────────────────────────
7+
8+ import {
9+ hashDynCols ,
10+ } from './useColumnVisibility.js' ;
11+
12+ import { highlight , download } from './utils.js' ;
13+
14+ const { ref, computed } = Vue ;
15+
16+ // ─── State ────────────────────────────────────────────────────────────────────
17+
18+ export const allHashResults = ref ( [ ] ) ;
19+ export const activeHashIdx = ref ( 0 ) ;
20+ export const hashSortCol = ref ( '#' ) ;
21+ export const hashSortAsc = ref ( true ) ;
22+ export const hashError = ref ( '' ) ;
23+ export const hashBulkCount = ref ( 0 ) ;
24+
25+ // ─── Derived ──────────────────────────────────────────────────────────────────
26+
27+ export const activeHashEntry = computed ( ( ) => allHashResults . value [ activeHashIdx . value ] || null ) ;
28+ export const activeHashResult = computed ( ( ) => activeHashEntry . value ?. result || activeHashEntry . value || null ) ;
29+
30+ export const hashResultLinks = computed ( ( ) => {
31+ const r = activeHashResult . value ;
32+ if ( ! r ) return { } ;
33+ if ( r . links && typeof r . links === 'object' ) return r . links ;
34+ return { } ;
35+ } ) ;
36+
37+ export const signerDetailObj = computed ( ( ) => {
38+ const d = activeHashResult . value ?. virustotal ?. signerDetail ;
39+ if ( ! d || typeof d !== 'object' ) return null ;
40+ return d ;
41+ } ) ;
42+
43+ export const signerIsRevoked = computed ( ( ) => {
44+ const s = signerDetailObj . value ?. status || '' ;
45+ return / r e v o k e d / i. test ( s ) ;
46+ } ) ;
47+
48+ export const signerIsInvalid = computed ( ( ) => {
49+ const s = signerDetailObj . value ?. status || '' ;
50+ return s . length > 0 && ! signerIsRevoked . value ;
51+ } ) ;
52+
53+ export const vtNotFound = computed ( ( ) => {
54+ const r = activeHashResult . value ;
55+ if ( ! r ) return false ;
56+ const vt = r . virustotal ;
57+ if ( ! vt ) return true ;
58+ const allZero = ( vt . malicious === 0 && vt . suspicious === 0 &&
59+ vt . harmless === 0 && vt . undetected === 0 ) ;
60+ const noMeta = ! vt . meaningfulName && ! vt . magic && ! vt . magika && ! vt . suggestedThreatLabel ;
61+ return allZero && noMeta ;
62+ } ) ;
63+
64+ export const highlightedHashJSON = computed ( ( ) => {
65+ if ( ! activeHashResult . value ) return '' ;
66+ return highlight ( JSON . stringify ( activeHashResult . value , null , 2 ) ) ;
67+ } ) ;
68+
69+ // ─── Table ────────────────────────────────────────────────────────────────────
70+
71+ export const visibleHashTableCols = computed ( ( ) => {
72+ const base = [ { key : '#' , label : '#' } , { key : 'hash' , label : 'Hash' } ] ;
73+ const dyn = hashDynCols . filter ( c => c . visible ) . map ( c => ( { key : c . key , label : c . label } ) ) ;
74+ return [ ...base , ...dyn ] ;
75+ } ) ;
76+
77+ export const sortedHashRows = computed ( ( ) => {
78+ const rows = allHashResults . value . map ( ( e , i ) => ( { ...( e . result || e ) , _idx : i , _hash : e . hash } ) ) ;
79+ rows . sort ( ( a , b ) => {
80+ const va = getHashCellVal ( a , hashSortCol . value ) ;
81+ const vb = getHashCellVal ( b , hashSortCol . value ) ;
82+ return ( va < vb ? - 1 : va > vb ? 1 : 0 ) * ( hashSortAsc . value ? 1 : - 1 ) ;
83+ } ) ;
84+ return rows ;
85+ } ) ;
86+
87+ export function sortHashTable ( key ) {
88+ if ( hashSortCol . value === key ) hashSortAsc . value = ! hashSortAsc . value ;
89+ else { hashSortCol . value = key ; hashSortAsc . value = true ; }
90+ }
91+
92+ export function getHashCellVal ( d , key ) {
93+ if ( key === '#' ) return d . _idx ;
94+ if ( key === 'hash' ) return d . _hash || d . virustotal ?. sha256 || d . virustotal ?. sha1 || d . virustotal ?. md5 || '' ;
95+ if ( key === 'riskLevel' ) return d . riskLevel || '—' ;
96+ if ( key === 'hashType' ) return d . hashType || '—' ;
97+ if ( key === 'link_virustotal' ) return d . links ?. virustotal || '—' ;
98+ if ( key === 'link_malwarebazaar' ) return d . links ?. malwarebazaar || '—' ;
99+
100+ const vtMap = {
101+ vtMalicious : 'malicious' , vtSuspicious : 'suspicious' , vtHarmless : 'harmless' ,
102+ vtUndetected : 'undetected' , vtReputation : 'reputation' ,
103+ meaningfulName : 'meaningfulName' , magic : 'magic' , magika : 'magika' ,
104+ md5 : 'md5' , sha1 : 'sha1' , sha256 : 'sha256' ,
105+ suggestedThreatLabel : 'suggestedThreatLabel' ,
106+ popularThreatCategories : 'popularThreatCategories' ,
107+ popularThreatNames : 'popularThreatNames' ,
108+ sandboxMalwareClassifications : 'sandboxMalwareClassifications' ,
109+ sigmaAnalysisSummary : 'sigmaAnalysisSummary' ,
110+ signatureSigners : 'signatureSigners' ,
111+ signerDetail : 'signerDetail' ,
112+ } ;
113+ if ( key in vtMap ) {
114+ const v = d . virustotal ?. [ vtMap [ key ] ] ;
115+ if ( v == null ) return '—' ;
116+ if ( Array . isArray ( v ) ) return v . join ( ', ' ) || '—' ;
117+ if ( typeof v === 'object' ) return JSON . stringify ( v ) ;
118+ return String ( v ) ;
119+ }
120+
121+ const mbMap = {
122+ mbQueryStatus : 'queryStatus' , mbFileName : 'fileName' , mbFileType : 'fileType' ,
123+ mbSignature : 'signature' , mbTags : 'tags' , mbComment : 'comment' ,
124+ } ;
125+ if ( key in mbMap ) {
126+ const v = d . malwarebazaar ?. [ mbMap [ key ] ] ;
127+ if ( v == null ) return '—' ;
128+ if ( Array . isArray ( v ) ) return v . join ( ', ' ) || '—' ;
129+ return String ( v ) ;
130+ }
131+
132+ const v = d [ key ] ;
133+ if ( v == null ) return '—' ;
134+ if ( Array . isArray ( v ) ) return v . join ( ', ' ) || '—' ;
135+ if ( typeof v === 'object' ) return JSON . stringify ( v ) ;
136+ return String ( v ) ;
137+ }
138+
139+ export function renderHashTableCell ( col , row ) {
140+ if ( col . key === '#' ) return String ( row . _idx + 1 ) ;
141+ if ( col . key === 'hash' ) {
142+ const h = row . _hash || row . virustotal ?. sha256 || row . virustotal ?. sha1 || row . virustotal ?. md5 || '?' ;
143+ return `<span style="font-size:0.65rem">${ h . slice ( 0 , 16 ) } …</span>` ;
144+ }
145+ if ( col . key === 'riskLevel' ) {
146+ const r = row . riskLevel || 'CLEAN' ;
147+ return `<span class="t-risk risk-${ r } ">${ r } </span>` ;
148+ }
149+ if ( col . key === 'mbQueryStatus' ) {
150+ const raw = row . malwarebazaar ?. queryStatus ;
151+ const color = raw === 'ok' ? '#34d399' : raw === 'hash_not_found' ? '#4d6480' : '#94a3b8' ;
152+ const label = raw === 'ok' ? 'Found' : raw === 'hash_not_found' ? 'Not found' : raw || '—' ;
153+ return `<span style="color:${ color } ;font-weight:${ raw === 'ok' ? 700 : 400 } ">${ label } </span>` ;
154+ }
155+ if ( col . key === 'link_virustotal' ) {
156+ const url = row . links ?. virustotal ;
157+ if ( ! url ) return `<span class="t-na">—</span>` ;
158+ const vt = row . virustotal || { } ;
159+ const notFound = vt . malicious === 0 && vt . suspicious === 0 && vt . harmless === 0 && vt . undetected === 0 && ! vt . meaningfulName ;
160+ return notFound
161+ ? `<span class="tbl-link-na" title="Not found in VirusTotal">✗ VT</span>`
162+ : `<a href="${ url } " target="_blank" rel="noopener" class="tbl-link-chip">↗ VT</a>` ;
163+ }
164+ if ( col . key === 'link_malwarebazaar' ) {
165+ const url = row . links ?. malwarebazaar ;
166+ if ( ! url ) return `<span class="t-na">—</span>` ;
167+ const notFound = row . malwarebazaar ?. queryStatus && row . malwarebazaar . queryStatus !== 'ok' ;
168+ return notFound
169+ ? `<span class="tbl-link-na" title="Not found in MalwareBazaar">✗ MB</span>`
170+ : `<a href="${ url } " target="_blank" rel="noopener" class="tbl-link-chip ab">↗ MB</a>` ;
171+ }
172+ const val = getHashCellVal ( row , col . key ) ;
173+ if ( val === '—' ) return `<span class="t-na">—</span>` ;
174+ if ( col . key === 'vtMalicious' && typeof val === 'string' && val !== '—' ) {
175+ const n = parseInt ( val ) ; const c = n >= 5 ? '#f87171' : n >= 1 ? '#fb923c' : '#34d399' ;
176+ return `<span style="color:${ c } ;font-weight:600">${ val } </span>` ;
177+ }
178+ if ( col . key === 'vtReputation' && typeof val === 'string' && val !== '—' ) {
179+ const n = parseInt ( val ) ; const c = n > 0 ? '#34d399' : n < 0 ? '#f87171' : '#4d6480' ;
180+ return `<span style="color:${ c } ">${ val } </span>` ;
181+ }
182+ const display = val . length > 60 ? val . slice ( 0 , 58 ) + '…' : val ;
183+ return `<span title="${ val . replace ( / " / g, '"' ) } ">${ display } </span>` ;
184+ }
185+
186+ // ─── IOC extraction ───────────────────────────────────────────────────────────
187+
188+ export function extractHashes ( text ) {
189+ const sha256 = / \b [ a - f A - F 0 - 9 ] { 64 } \b / g;
190+ const sha1 = / \b [ a - f A - F 0 - 9 ] { 40 } \b / g;
191+ const md5 = / \b [ a - f A - F 0 - 9 ] { 32 } \b / g;
192+ const found = [ ...( text . match ( sha256 ) || [ ] ) , ...( text . match ( sha1 ) || [ ] ) , ...( text . match ( md5 ) || [ ] ) ] ;
193+ return [ ...new Map ( found . map ( h => [ h . toLowerCase ( ) , h . toLowerCase ( ) ] ) ) . values ( ) ] ;
194+ }
195+
196+ export function clearHashBulk ( hashInputTextRef ) {
197+ hashInputTextRef . value = '' ;
198+ hashBulkCount . value = 0 ;
199+ }
200+
201+ // ─── Export / copy ────────────────────────────────────────────────────────────
202+
203+ export function copyHashJSON ( activeHashResultVal ) {
204+ if ( ! activeHashResultVal ) return ;
205+ navigator . clipboard . writeText ( JSON . stringify ( activeHashResultVal , null , 2 ) ) ;
206+ }
207+
208+ export async function copyHashClipboard ( format , hashCopyMenuOpenRef ) {
209+ hashCopyMenuOpenRef . value = false ;
210+ const rows = allHashResults . value . filter ( e => e . result ) . map ( e => e . result || e ) ;
211+ if ( ! rows . length ) return ;
212+ let text = '' ;
213+ if ( format === 'json' ) {
214+ text = JSON . stringify ( rows . map ( _buildHashExportRow ) , null , 2 ) ;
215+ } else if ( format === 'csv' ) {
216+ const { header, lines } = _buildHashCSV ( rows ) ;
217+ text = [ header , ...lines ] . join ( '\n' ) ;
218+ } else if ( format === 'hashes' ) {
219+ text = rows . map ( r => r . virustotal ?. sha256 || r . virustotal ?. sha1 || r . virustotal ?. md5 || r . _hash || r . hash ) . filter ( Boolean ) . join ( '\n' ) ;
220+ }
221+ try {
222+ await navigator . clipboard . writeText ( text ) ;
223+ const btn = document . getElementById ( 'hashClipboardBtn' ) ;
224+ if ( btn ) { const orig = btn . textContent ; btn . textContent = '✓ Copied!' ; setTimeout ( ( ) => btn . textContent = orig , 2000 ) ; }
225+ } catch ( e ) {
226+ console . warn ( 'Clipboard write failed:' , e ) ;
227+ }
228+ }
229+
230+ export function exportHashCSV ( ) {
231+ const rows = allHashResults . value . filter ( e => e . result ) . map ( e => e . result || e ) ;
232+ if ( ! rows . length ) return ;
233+ const { header, lines } = _buildHashCSV ( rows ) ;
234+ download ( [ header , ...lines ] . join ( '\n' ) , 'iocscan_hash_results.csv' , 'text/csv' ) ;
235+ }
236+
237+ export function exportHashJSON ( ) {
238+ const rows = allHashResults . value . filter ( e => e . result ) . map ( e => e . result || e ) ;
239+ if ( ! rows . length ) return ;
240+ download ( JSON . stringify ( rows . map ( _buildHashExportRow ) , null , 2 ) , 'iocscan_hash_results.json' , 'application/json' ) ;
241+ }
242+
243+ // ─── Private helpers ──────────────────────────────────────────────────────────
244+
245+ function _buildHashExportRow ( row ) {
246+ const cols = hashDynCols . filter ( c => c . visible ) ;
247+ const obj = { } ;
248+ cols . forEach ( c => {
249+ let v = getHashCellVal ( row , c . key ) ;
250+ if ( v === '—' ) v = null ;
251+ const exportKey = c . key === 'link_virustotal' ? 'virustotal_link'
252+ : c . key === 'link_malwarebazaar' ? 'malwarebazaar_link'
253+ : c . key ;
254+ obj [ exportKey ] = v ;
255+ } ) ;
256+ return obj ;
257+ }
258+
259+ function _buildHashCSV ( rows ) {
260+ const cols = hashDynCols . filter ( c => c . visible ) ;
261+ const header = cols . map ( c => c . label ) . join ( ',' ) ;
262+ const lines = rows . map ( row => cols . map ( c => {
263+ let v = getHashCellVal ( row , c . key ) ;
264+ if ( v == null || v === '—' ) v = '' ;
265+ return '"' + String ( v ) . replace ( / " / g, '""' ) + '"' ;
266+ } ) . join ( ',' ) ) ;
267+ return { header, lines } ;
268+ }
0 commit comments