Skip to content

Commit 22e345c

Browse files
committed
fix(scan): normalize IOC input handling
- Deduplicate IP, domain, and hash inputs before scan - Defang bracketed IP/domain dots - Accept string is_crawler values from ipapi.is - Remove API keys info bar and allow scan box resize
1 parent 9e797df commit 22e345c

6 files changed

Lines changed: 88 additions & 52 deletions

File tree

‎integrations/ipapi.go‎

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,60 @@ import (
1111
"encoding/json"
1212
"fmt"
1313
"net/url"
14+
"strconv"
15+
"strings"
1416

1517
"github.com/TwoA2U/iocscan/internal/httpclient"
1618
)
1719

1820
const ipapiEndpoint = "https://api.ipapi.is"
1921

22+
type flexibleBool bool
23+
24+
func (b *flexibleBool) UnmarshalJSON(data []byte) error {
25+
var raw any
26+
if err := json.Unmarshal(data, &raw); err != nil {
27+
return err
28+
}
29+
30+
switch v := raw.(type) {
31+
case bool:
32+
*b = flexibleBool(v)
33+
case string:
34+
s := strings.TrimSpace(strings.ToLower(v))
35+
if s == "" || s == "false" || s == "0" || s == "no" || s == "none" || s == "null" {
36+
*b = false
37+
return nil
38+
}
39+
if parsed, err := strconv.ParseBool(s); err == nil {
40+
*b = flexibleBool(parsed)
41+
return nil
42+
}
43+
*b = true
44+
case float64:
45+
*b = flexibleBool(v != 0)
46+
case nil:
47+
*b = false
48+
default:
49+
*b = false
50+
}
51+
return nil
52+
}
53+
2054
// IPAPIResponse is the raw response from ipapi.is.
2155
type IPAPIResponse struct {
22-
IP string `json:"ip"`
23-
RIR string `json:"rir"`
24-
IsBogon bool `json:"is_bogon"`
25-
IsMobile bool `json:"is_mobile"`
26-
IsSatellite bool `json:"is_satellite"`
27-
IsCrawler bool `json:"is_crawler"`
28-
IsDatacenter bool `json:"is_datacenter"`
29-
IsTor bool `json:"is_tor"`
30-
IsProxy bool `json:"is_proxy"`
31-
IsVPN bool `json:"is_vpn"`
32-
IsAbuser bool `json:"is_abuser"`
33-
VPN struct {
56+
IP string `json:"ip"`
57+
RIR string `json:"rir"`
58+
IsBogon bool `json:"is_bogon"`
59+
IsMobile bool `json:"is_mobile"`
60+
IsSatellite bool `json:"is_satellite"`
61+
IsCrawler flexibleBool `json:"is_crawler"`
62+
IsDatacenter bool `json:"is_datacenter"`
63+
IsTor bool `json:"is_tor"`
64+
IsProxy bool `json:"is_proxy"`
65+
IsVPN bool `json:"is_vpn"`
66+
IsAbuser bool `json:"is_abuser"`
67+
VPN struct {
3468
Service string `json:"service"`
3569
Type string `json:"type"`
3670
LastSeenStr string `json:"last_seen_str"`
@@ -47,13 +81,13 @@ type IPAPIResponse struct {
4781
Org string `json:"org"`
4882
} `json:"asn"`
4983
Location struct {
50-
Country string `json:"country"`
84+
Country string `json:"country"`
5185
CountryCode string `json:"country_code"`
52-
State string `json:"state"`
53-
City string `json:"city"`
54-
Timezone string `json:"timezone"`
55-
LocalTime string `json:"local_time"`
56-
IsDST bool `json:"is_dst"`
86+
State string `json:"state"`
87+
City string `json:"city"`
88+
Timezone string `json:"timezone"`
89+
LocalTime string `json:"local_time"`
90+
IsDST bool `json:"is_dst"`
5791
} `json:"location"`
5892
}
5993

@@ -186,28 +220,28 @@ func (i IPAPIIntegration) Run(ctx context.Context, ioc, apiKey string, useCache
186220

187221
func ipapiToResult(r *IPAPIResponse) *Result {
188222
return &Result{Fields: map[string]any{
189-
"country": r.Location.Country,
190-
"city": r.Location.City,
191-
"state": r.Location.State,
192-
"timezone": r.Location.Timezone,
193-
"localTime": r.Location.LocalTime,
194-
"isDST": r.Location.IsDST,
195-
"org": r.ASN.Org,
196-
"companyName": r.Company.Name,
197-
"companyType": r.Company.Type,
223+
"country": r.Location.Country,
224+
"city": r.Location.City,
225+
"state": r.Location.State,
226+
"timezone": r.Location.Timezone,
227+
"localTime": r.Location.LocalTime,
228+
"isDST": r.Location.IsDST,
229+
"org": r.ASN.Org,
230+
"companyName": r.Company.Name,
231+
"companyType": r.Company.Type,
198232
"companyDomain": r.Company.Domain,
199-
"abuserScore": r.Company.AbuserScore,
200-
"isBogon": r.IsBogon,
201-
"isMobile": r.IsMobile,
202-
"isSatellite": r.IsSatellite,
203-
"isCrawler": r.IsCrawler,
204-
"isDatacenter": r.IsDatacenter,
205-
"isTor": r.IsTor,
206-
"isProxy": r.IsProxy,
207-
"isVPN": r.IsVPN,
208-
"isAbuser": r.IsAbuser,
209-
"vpnService": r.VPN.Service,
210-
"vpnType": r.VPN.Type,
211-
"vpnLastSeen": r.VPN.LastSeenStr,
233+
"abuserScore": r.Company.AbuserScore,
234+
"isBogon": r.IsBogon,
235+
"isMobile": r.IsMobile,
236+
"isSatellite": r.IsSatellite,
237+
"isCrawler": bool(r.IsCrawler),
238+
"isDatacenter": r.IsDatacenter,
239+
"isTor": r.IsTor,
240+
"isProxy": r.IsProxy,
241+
"isVPN": r.IsVPN,
242+
"isAbuser": r.IsAbuser,
243+
"vpnService": r.VPN.Service,
244+
"vpnType": r.VPN.Type,
245+
"vpnLastSeen": r.VPN.LastSeenStr,
212246
}}
213247
}

‎web/components/IOCScanner.js‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,6 @@ export default defineComponent({
315315
<!-- ══ PAGE BODY ══════════════════════════════════════════════════ -->
316316
<div class="px-8 py-8 pb-20" id="shell">
317317
318-
<div class="keys-panel">
319-
<span class="keys-panel-label">API Keys</span>
320-
<div class="text-sm text-t2">
321-
API keys are managed in Settings for the signed-in account.
322-
</div>
323-
</div>
324-
325318
<!-- Mode tabs -->
326319
<div class="flex border-b border-white/[0.08] mb-6">
327320
<button class="ioc-tab" :class="{active: currentIOCMode==='ip'}" @click="switchIOCMode('ip')">IP Address</button>

‎web/composables/useDomainResults.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ const reDomain = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]
114114
const reIPv4 = /^(\d{1,3}\.){3}\d{1,3}$/;
115115

116116
export function extractDomains(text) {
117-
const tokens = text.split(/[\s,;\n\r\t]+/).map(t => t.trim()).filter(Boolean);
117+
const normalized = String(text || '').replace(/\[\.\]/g, '.');
118+
const tokens = normalized.split(/[\s,;\n\r\t]+/).map(t => t.trim()).filter(Boolean);
118119
const seen = new Set();
119120
const result = [];
120121
for (const t of tokens) {

‎web/composables/useIOCScan.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,11 @@ export function vtStatPart(idx) {
272272
// ─── API calls ────────────────────────────────────────────────────────────────
273273

274274
export async function doIPScan() {
275-
const ip = ipInputText.value.trim();
276-
if (!ip) { IPResults.ipError.value = 'Enter at least one IP address.'; return; }
275+
const ips = IPResults.extractIPs(ipInputText.value);
276+
if (!ips.length) { IPResults.ipError.value = 'Enter at least one IP address.'; return; }
277+
const ip = ips.join('\n');
278+
ipInputText.value = ip;
279+
IPResults.ipBulkCount.value = ips.length;
277280
IPResults.ipError.value = '';
278281
isIPLoading.value = true;
279282
try {
@@ -306,6 +309,8 @@ export async function doHashScanAction() {
306309
if (!raw) { HashResults.hashError.value = 'Please enter at least one hash.'; return; }
307310
const hashes = HashResults.extractHashes(raw);
308311
if (!hashes.length) { HashResults.hashError.value = 'No valid MD5/SHA1/SHA256 hashes found.'; return; }
312+
hashInputText.value = hashes.join('\n');
313+
HashResults.hashBulkCount.value = hashes.length;
309314
HashResults.hashError.value = '';
310315
isHashLoading.value = true;
311316
try {
@@ -336,6 +341,8 @@ export async function doDomainScan() {
336341
if (!raw) { DomainResults.domainError.value = 'Enter at least one domain.'; return; }
337342
const domains = DomainResults.extractDomains(raw);
338343
if (!domains.length) { DomainResults.domainError.value = 'No valid domains found.'; return; }
344+
DomainResults.domainInputText.value = domains.join('\n');
345+
DomainResults.domainBulkCount.value = domains.length;
339346
DomainResults.domainError.value = '';
340347
DomainResults.isDomainLoading.value = true;
341348
try {

‎web/composables/useIPResults.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@ export function renderTableCell(col, row, i) {
341341
export function extractIPs(text) {
342342
const ipv4 = /\b(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\b/g;
343343
const ipv6 = /\b([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g;
344-
const found = [...(text.match(ipv4) || []), ...(text.match(ipv6) || [])];
344+
const normalized = String(text || '').replace(/\[\.\]/g, '.');
345+
const found = [...(normalized.match(ipv4) || []), ...(normalized.match(ipv6) || [])];
345346
return [...new Map(found.map(ip => [ip, ip])).values()];
346347
}
347348

‎web/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@
486486
border: 1px solid rgba(255,255,255,0.10); border-radius: 8px;
487487
color: #f0f1f5; font-family: 'JetBrains Mono', monospace;
488488
font-size: 0.82rem; padding: 11px 14px;
489-
resize: none; min-height: 46px; outline: none;
489+
resize: vertical; min-height: 46px; outline: none;
490490
transition: border-color 0.2s, box-shadow 0.2s; line-height: 1.5;
491491
}
492492
.scan-input:focus { border-color: rgba(129,140,248,0.4); box-shadow: 0 0 0 3px rgba(129,140,248,0.08); }

0 commit comments

Comments
 (0)