Skip to content

Commit 78759fe

Browse files
committed
fix: Add forgotten files
1 parent c6ebe76 commit 78759fe

5 files changed

Lines changed: 641 additions & 0 deletions

File tree

utils/iocutil.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// utils/iocutil.go — IOC type detection helper.
2+
//
3+
// Kept as a separate file for future expansion (domain/hash scanning).
4+
// The actual API fetching lives in iputil.go.
5+
package utils
6+
7+
import (
8+
"net"
9+
"regexp"
10+
"strings"
11+
)
12+
13+
// IOCType classifies what kind of indicator we are dealing with.
14+
type IOCType string
15+
16+
const (
17+
TypeIP IOCType = "ip"
18+
TypeDomain IOCType = "domain"
19+
TypeHash IOCType = "hash"
20+
TypeUnknown IOCType = "unknown"
21+
)
22+
23+
var (
24+
reMD5 = regexp.MustCompile(`^[a-fA-F0-9]{32}$`)
25+
reSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
26+
reSHA256 = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
27+
reDomain = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
28+
)
29+
30+
// DetectIOCType classifies the given string as IP, domain, hash, or unknown.
31+
func DetectIOCType(ioc string) IOCType {
32+
ioc = strings.TrimSpace(ioc)
33+
if net.ParseIP(ioc) != nil {
34+
return TypeIP
35+
}
36+
if reMD5.MatchString(ioc) || reSHA1.MatchString(ioc) || reSHA256.MatchString(ioc) {
37+
return TypeHash
38+
}
39+
if reDomain.MatchString(ioc) {
40+
return TypeDomain
41+
}
42+
return TypeUnknown
43+
}

web/composables/useHashResults.js

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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 /revoked/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, '&quot;')}">${display}</span>`;
184+
}
185+
186+
// ─── IOC extraction ───────────────────────────────────────────────────────────
187+
188+
export function extractHashes(text) {
189+
const sha256 = /\b[a-fA-F0-9]{64}\b/g;
190+
const sha1 = /\b[a-fA-F0-9]{40}\b/g;
191+
const md5 = /\b[a-fA-F0-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

Comments
 (0)