domsniper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +40 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/package.json +72 -0
- package/src/app.tsx +2062 -0
- package/src/completions.ts +65 -0
- package/src/core/db.ts +1313 -0
- package/src/core/features/asn-lookup.ts +91 -0
- package/src/core/features/backlinks.ts +83 -0
- package/src/core/features/blacklist-check.ts +67 -0
- package/src/core/features/cert-transparency.ts +87 -0
- package/src/core/features/config.ts +81 -0
- package/src/core/features/cors-check.ts +90 -0
- package/src/core/features/dns-details.ts +27 -0
- package/src/core/features/domain-age.ts +33 -0
- package/src/core/features/domain-suggest.ts +87 -0
- package/src/core/features/drop-catch.ts +159 -0
- package/src/core/features/email-security.ts +112 -0
- package/src/core/features/expiring-feed.ts +160 -0
- package/src/core/features/export.ts +74 -0
- package/src/core/features/filter.ts +96 -0
- package/src/core/features/http-probe.ts +46 -0
- package/src/core/features/marketplace.ts +69 -0
- package/src/core/features/path-scanner.ts +123 -0
- package/src/core/features/port-scanner.ts +132 -0
- package/src/core/features/portfolio-bulk.ts +125 -0
- package/src/core/features/portfolio-monitor.ts +214 -0
- package/src/core/features/portfolio.ts +98 -0
- package/src/core/features/price-compare.ts +39 -0
- package/src/core/features/rdap.ts +128 -0
- package/src/core/features/reverse-ip.ts +73 -0
- package/src/core/features/s3-export.ts +99 -0
- package/src/core/features/scoring.ts +121 -0
- package/src/core/features/security-headers.ts +162 -0
- package/src/core/features/session.ts +74 -0
- package/src/core/features/snipe.ts +264 -0
- package/src/core/features/social-check.ts +81 -0
- package/src/core/features/ssl-check.ts +88 -0
- package/src/core/features/subdomain-discovery.ts +53 -0
- package/src/core/features/takeover-detect.ts +143 -0
- package/src/core/features/tech-stack.ts +135 -0
- package/src/core/features/tld-expand.ts +43 -0
- package/src/core/features/variations.ts +134 -0
- package/src/core/features/version-check.ts +58 -0
- package/src/core/features/waf-detect.ts +171 -0
- package/src/core/features/watch.ts +120 -0
- package/src/core/features/wayback.ts +64 -0
- package/src/core/features/webhooks.ts +126 -0
- package/src/core/features/whois-history.ts +99 -0
- package/src/core/features/zone-transfer.ts +75 -0
- package/src/core/index.ts +50 -0
- package/src/core/paths.ts +9 -0
- package/src/core/registrar.ts +413 -0
- package/src/core/theme.ts +140 -0
- package/src/core/types.ts +143 -0
- package/src/core/validate.ts +58 -0
- package/src/core/whois.ts +265 -0
- package/src/index.tsx +1888 -0
- package/src/market-client.ts +186 -0
- package/src/proxy/ca.ts +116 -0
- package/src/proxy/db.ts +175 -0
- package/src/proxy/server.ts +155 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { assertValidDomain } from "../validate.js";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export interface AsnResult {
|
|
8
|
+
domain: string;
|
|
9
|
+
ip: string | null;
|
|
10
|
+
asn: string | null;
|
|
11
|
+
asnName: string | null;
|
|
12
|
+
org: string | null;
|
|
13
|
+
country: string | null;
|
|
14
|
+
city: string | null;
|
|
15
|
+
region: string | null;
|
|
16
|
+
isp: string | null;
|
|
17
|
+
error: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function lookupAsn(domain: string): Promise<AsnResult> {
|
|
21
|
+
assertValidDomain(domain);
|
|
22
|
+
|
|
23
|
+
const result: AsnResult = {
|
|
24
|
+
domain, ip: null, asn: null, asnName: null,
|
|
25
|
+
org: null, country: null, city: null, region: null, isp: null, error: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Resolve IP
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await execFileAsync("dig", ["+short", domain, "A"], { timeout: 5000 });
|
|
32
|
+
result.ip = stdout.trim().split("\n")[0] || null;
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
if (!result.ip) {
|
|
36
|
+
result.error = "Could not resolve IP";
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Use ip-api.com (free, 45 req/min, no key needed)
|
|
41
|
+
try {
|
|
42
|
+
const resp = await fetch(
|
|
43
|
+
`http://ip-api.com/json/${encodeURIComponent(result.ip)}?fields=status,country,regionName,city,isp,org,as,asname`,
|
|
44
|
+
{ signal: AbortSignal.timeout(8000) }
|
|
45
|
+
);
|
|
46
|
+
const data = await resp.json() as {
|
|
47
|
+
status?: string;
|
|
48
|
+
country?: string;
|
|
49
|
+
regionName?: string;
|
|
50
|
+
city?: string;
|
|
51
|
+
isp?: string;
|
|
52
|
+
org?: string;
|
|
53
|
+
as?: string;
|
|
54
|
+
asname?: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (data.status === "success") {
|
|
58
|
+
result.country = data.country || null;
|
|
59
|
+
result.region = data.regionName || null;
|
|
60
|
+
result.city = data.city || null;
|
|
61
|
+
result.isp = data.isp || null;
|
|
62
|
+
result.org = data.org || null;
|
|
63
|
+
result.asn = data.as || null;
|
|
64
|
+
result.asnName = data.asname || null;
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
|
|
68
|
+
// Fallback: DNS-based ASN lookup via Team Cymru
|
|
69
|
+
if (!result.asn && result.ip) {
|
|
70
|
+
try {
|
|
71
|
+
const reversed = result.ip.split(".").reverse().join(".");
|
|
72
|
+
const { stdout } = await execFileAsync(
|
|
73
|
+
"dig", ["+short", `${reversed}.origin.asn.cymru.com`, "TXT"],
|
|
74
|
+
{ timeout: 5000 }
|
|
75
|
+
);
|
|
76
|
+
const txt = stdout.trim().replace(/"/g, "");
|
|
77
|
+
// Format: "ASN | IP/Prefix | CC | Registry | Date"
|
|
78
|
+
const parts = txt.split("|").map((p) => p.trim());
|
|
79
|
+
if (parts.length >= 3) {
|
|
80
|
+
result.asn = parts[0] ? `AS${parts[0]}` : null;
|
|
81
|
+
result.country = parts[2] || null;
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
} catch (err: unknown) {
|
|
88
|
+
result.error = err instanceof Error ? err.message : "ASN lookup failed";
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { assertValidDomain } from "../validate.js";
|
|
2
|
+
|
|
3
|
+
export interface BacklinkResult {
|
|
4
|
+
domain: string;
|
|
5
|
+
estimatedBacklinks: number | null;
|
|
6
|
+
pageRank: number | null;
|
|
7
|
+
commonCrawlPages: number | null;
|
|
8
|
+
sources: BacklinkSource[];
|
|
9
|
+
error: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BacklinkSource {
|
|
13
|
+
name: string;
|
|
14
|
+
value: number | null;
|
|
15
|
+
error: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function checkCommonCrawl(domain: string): Promise<BacklinkSource> {
|
|
19
|
+
try {
|
|
20
|
+
// Use Common Crawl index API to estimate pages
|
|
21
|
+
const resp = await fetch(
|
|
22
|
+
`https://index.commoncrawl.org/CC-MAIN-2025-51-index?url=*.${encodeURIComponent(domain)}&output=json&limit=1&showNumPages=true`,
|
|
23
|
+
{ signal: AbortSignal.timeout(10000) }
|
|
24
|
+
);
|
|
25
|
+
if (!resp.ok) {
|
|
26
|
+
return { name: "CommonCrawl", value: null, error: `HTTP ${resp.status}` };
|
|
27
|
+
}
|
|
28
|
+
const text = await resp.text();
|
|
29
|
+
// The showNumPages response is a single number
|
|
30
|
+
const pages = parseInt(text.trim(), 10);
|
|
31
|
+
if (!isNaN(pages)) {
|
|
32
|
+
return { name: "CommonCrawl", value: pages, error: null };
|
|
33
|
+
}
|
|
34
|
+
// Try parsing as JSON lines
|
|
35
|
+
const lines = text.trim().split("\n").filter(Boolean);
|
|
36
|
+
return { name: "CommonCrawl", value: lines.length, error: null };
|
|
37
|
+
} catch (err: unknown) {
|
|
38
|
+
return { name: "CommonCrawl", value: null, error: err instanceof Error ? err.message : "Failed" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function checkOpenPageRank(domain: string): Promise<BacklinkSource> {
|
|
43
|
+
try {
|
|
44
|
+
// Open PageRank - free API, no key needed for basic lookups
|
|
45
|
+
const resp = await fetch(
|
|
46
|
+
`https://openpagerank.com/api/v1.0/getPageRank?domains[]=${encodeURIComponent(domain)}`,
|
|
47
|
+
{
|
|
48
|
+
signal: AbortSignal.timeout(8000),
|
|
49
|
+
headers: { "User-Agent": "DomainSniper/2.0" },
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
if (!resp.ok) {
|
|
53
|
+
return { name: "OpenPageRank", value: null, error: `HTTP ${resp.status}` };
|
|
54
|
+
}
|
|
55
|
+
const data = await resp.json() as {
|
|
56
|
+
status_code?: number;
|
|
57
|
+
response?: Array<{ page_rank_decimal?: number; rank?: number }>;
|
|
58
|
+
};
|
|
59
|
+
const first = data.response?.[0];
|
|
60
|
+
const rank = first?.page_rank_decimal ?? first?.rank ?? null;
|
|
61
|
+
return { name: "OpenPageRank", value: typeof rank === "number" ? rank : null, error: null };
|
|
62
|
+
} catch (err: unknown) {
|
|
63
|
+
return { name: "OpenPageRank", value: null, error: err instanceof Error ? err.message : "Failed" };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function estimateBacklinks(domain: string): Promise<BacklinkResult> {
|
|
68
|
+
assertValidDomain(domain);
|
|
69
|
+
|
|
70
|
+
const [ccResult, prResult] = await Promise.all([
|
|
71
|
+
checkCommonCrawl(domain),
|
|
72
|
+
checkOpenPageRank(domain),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
domain,
|
|
77
|
+
estimatedBacklinks: ccResult.value,
|
|
78
|
+
pageRank: prResult.value,
|
|
79
|
+
commonCrawlPages: ccResult.value,
|
|
80
|
+
sources: [ccResult, prResult],
|
|
81
|
+
error: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { assertValidDomain } from "../validate.js";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export interface BlacklistResult {
|
|
8
|
+
domain: string;
|
|
9
|
+
listed: boolean;
|
|
10
|
+
lists: BlacklistEntry[];
|
|
11
|
+
cleanCount: number;
|
|
12
|
+
listedCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BlacklistEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
listed: boolean;
|
|
18
|
+
detail: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// DNS-based blacklists for domain reputation
|
|
22
|
+
const DOMAIN_BLACKLISTS = [
|
|
23
|
+
{ name: "Spamhaus DBL", suffix: "dbl.spamhaus.org" },
|
|
24
|
+
{ name: "SURBL", suffix: "multi.surbl.org" },
|
|
25
|
+
{ name: "URIBL", suffix: "multi.uribl.com" },
|
|
26
|
+
{ name: "Spamhaus ZEN", suffix: "zen.spamhaus.org" },
|
|
27
|
+
{ name: "Barracuda", suffix: "b.barracudacentral.org" },
|
|
28
|
+
{ name: "SpamCop", suffix: "bl.spamcop.net" },
|
|
29
|
+
{ name: "PhishTank", suffix: "phishtank.org" },
|
|
30
|
+
{ name: "SORBS", suffix: "dnsbl.sorbs.net" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
async function checkBlacklist(
|
|
34
|
+
domain: string,
|
|
35
|
+
bl: { name: string; suffix: string }
|
|
36
|
+
): Promise<BlacklistEntry> {
|
|
37
|
+
try {
|
|
38
|
+
const query = `${domain}.${bl.suffix}`;
|
|
39
|
+
const { stdout } = await execFileAsync("dig", ["+short", query, "A"], { timeout: 5000 });
|
|
40
|
+
const result = stdout.trim();
|
|
41
|
+
// A response (typically 127.0.0.x) means listed
|
|
42
|
+
if (result && result.startsWith("127.")) {
|
|
43
|
+
return { name: bl.name, listed: true, detail: result };
|
|
44
|
+
}
|
|
45
|
+
return { name: bl.name, listed: false, detail: null };
|
|
46
|
+
} catch {
|
|
47
|
+
// NXDOMAIN or timeout = not listed (which is good)
|
|
48
|
+
return { name: bl.name, listed: false, detail: null };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function checkBlacklists(domain: string): Promise<BlacklistResult> {
|
|
53
|
+
assertValidDomain(domain);
|
|
54
|
+
|
|
55
|
+
const results = await Promise.all(
|
|
56
|
+
DOMAIN_BLACKLISTS.map((bl) => checkBlacklist(domain, bl))
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const listedCount = results.filter((r) => r.listed).length;
|
|
60
|
+
return {
|
|
61
|
+
domain,
|
|
62
|
+
listed: listedCount > 0,
|
|
63
|
+
lists: results,
|
|
64
|
+
cleanCount: results.length - listedCount,
|
|
65
|
+
listedCount,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { assertValidDomain } from "../validate.js";
|
|
2
|
+
|
|
3
|
+
export interface CertTransparencyResult {
|
|
4
|
+
domain: string;
|
|
5
|
+
subdomains: string[];
|
|
6
|
+
certificates: CertEntry[];
|
|
7
|
+
totalCerts: number;
|
|
8
|
+
error: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CertEntry {
|
|
12
|
+
commonName: string;
|
|
13
|
+
issuer: string;
|
|
14
|
+
notBefore: string;
|
|
15
|
+
notAfter: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function queryCertTransparency(domain: string): Promise<CertTransparencyResult> {
|
|
19
|
+
assertValidDomain(domain);
|
|
20
|
+
|
|
21
|
+
const result: CertTransparencyResult = {
|
|
22
|
+
domain,
|
|
23
|
+
subdomains: [],
|
|
24
|
+
certificates: [],
|
|
25
|
+
totalCerts: 0,
|
|
26
|
+
error: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const resp = await fetch(
|
|
31
|
+
`https://crt.sh/?q=%25.${encodeURIComponent(domain)}&output=json`,
|
|
32
|
+
{ signal: AbortSignal.timeout(15000) }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (!resp.ok) {
|
|
36
|
+
result.error = `crt.sh returned HTTP ${resp.status}`;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const data = await resp.json() as Array<{
|
|
41
|
+
common_name?: string;
|
|
42
|
+
name_value?: string;
|
|
43
|
+
issuer_name?: string;
|
|
44
|
+
not_before?: string;
|
|
45
|
+
not_after?: string;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
result.totalCerts = data.length;
|
|
49
|
+
|
|
50
|
+
// Extract unique subdomains
|
|
51
|
+
const subdomainSet = new Set<string>();
|
|
52
|
+
for (const cert of data) {
|
|
53
|
+
if (cert.common_name) {
|
|
54
|
+
const cn = cert.common_name.toLowerCase().replace(/^\*\./, "");
|
|
55
|
+
if (cn.endsWith(domain) || cn === domain) subdomainSet.add(cn);
|
|
56
|
+
}
|
|
57
|
+
if (cert.name_value) {
|
|
58
|
+
const names = cert.name_value.split("\n");
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
const clean = name.trim().toLowerCase().replace(/^\*\./, "");
|
|
61
|
+
if (clean.endsWith(domain) || clean === domain) subdomainSet.add(clean);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
result.subdomains = Array.from(subdomainSet).sort();
|
|
67
|
+
|
|
68
|
+
// Keep recent certs (deduplicated by common name)
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
for (const cert of data.slice(0, 50)) {
|
|
71
|
+
const cn = cert.common_name || "";
|
|
72
|
+
if (seen.has(cn)) continue;
|
|
73
|
+
seen.add(cn);
|
|
74
|
+
result.certificates.push({
|
|
75
|
+
commonName: cn,
|
|
76
|
+
issuer: cert.issuer_name || "",
|
|
77
|
+
notBefore: cert.not_before || "",
|
|
78
|
+
notAfter: cert.not_after || "",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
result.error = err instanceof Error ? err.message : "Certificate transparency lookup failed";
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import type { RegistrarProvider } from "../registrar.js";
|
|
3
|
+
import { APP_DIR, CONFIG_FILE } from "../paths.js";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = APP_DIR;
|
|
6
|
+
|
|
7
|
+
export interface DomainSniperConfig {
|
|
8
|
+
concurrency: number;
|
|
9
|
+
rateLimitMs: number;
|
|
10
|
+
defaultTldPreset: "popular" | "premium" | "startup" | "all";
|
|
11
|
+
registrar: {
|
|
12
|
+
provider: RegistrarProvider;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
apiSecret: string;
|
|
15
|
+
accountId: string;
|
|
16
|
+
username: string;
|
|
17
|
+
clientIp: string;
|
|
18
|
+
} | null;
|
|
19
|
+
notifications: {
|
|
20
|
+
webhookUrl: string | null;
|
|
21
|
+
emailTo: string | null;
|
|
22
|
+
smtpHost: string | null;
|
|
23
|
+
smtpPort: number;
|
|
24
|
+
smtpUser: string | null;
|
|
25
|
+
smtpPass: string | null;
|
|
26
|
+
};
|
|
27
|
+
watch: {
|
|
28
|
+
intervalMs: number;
|
|
29
|
+
desktopNotify: boolean;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_CONFIG: DomainSniperConfig = {
|
|
34
|
+
concurrency: 5,
|
|
35
|
+
rateLimitMs: 500,
|
|
36
|
+
defaultTldPreset: "popular",
|
|
37
|
+
registrar: null,
|
|
38
|
+
notifications: {
|
|
39
|
+
webhookUrl: null,
|
|
40
|
+
emailTo: null,
|
|
41
|
+
smtpHost: null,
|
|
42
|
+
smtpPort: 587,
|
|
43
|
+
smtpUser: null,
|
|
44
|
+
smtpPass: null,
|
|
45
|
+
},
|
|
46
|
+
watch: {
|
|
47
|
+
intervalMs: 3600000,
|
|
48
|
+
desktopNotify: true,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function ensureDir(): void {
|
|
53
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
54
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function loadConfig(): DomainSniperConfig {
|
|
59
|
+
ensureDir();
|
|
60
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
61
|
+
try {
|
|
62
|
+
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
63
|
+
const parsed = JSON.parse(content);
|
|
64
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
65
|
+
} catch {
|
|
66
|
+
return { ...DEFAULT_CONFIG };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveConfig(config: DomainSniperConfig): void {
|
|
71
|
+
ensureDir();
|
|
72
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getConfigPath(): string {
|
|
76
|
+
return CONFIG_FILE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resetConfig(): void {
|
|
80
|
+
saveConfig(DEFAULT_CONFIG);
|
|
81
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { assertValidDomain } from "../validate.js";
|
|
2
|
+
|
|
3
|
+
export interface CorsResult {
|
|
4
|
+
domain: string;
|
|
5
|
+
vulnerable: boolean;
|
|
6
|
+
findings: CorsFinding[];
|
|
7
|
+
error: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CorsFinding {
|
|
11
|
+
test: string;
|
|
12
|
+
origin: string;
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
credentials: boolean;
|
|
15
|
+
severity: "critical" | "high" | "medium" | "low" | "info";
|
|
16
|
+
detail: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CORS_TESTS: { name: string; origin: string; severity: "critical" | "high" | "medium" }[] = [
|
|
20
|
+
{ name: "Wildcard origin", origin: "https://evil.com", severity: "critical" },
|
|
21
|
+
{ name: "Null origin", origin: "null", severity: "high" },
|
|
22
|
+
{ name: "Subdomain reflection", origin: "https://evil.TARGET", severity: "high" },
|
|
23
|
+
{ name: "Prefix match bypass", origin: "https://TARGETevil.com", severity: "medium" },
|
|
24
|
+
{ name: "Suffix match bypass", origin: "https://evil-TARGET", severity: "medium" },
|
|
25
|
+
{ name: "HTTP downgrade", origin: "http://TARGET", severity: "medium" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export async function checkCors(domain: string): Promise<CorsResult> {
|
|
29
|
+
assertValidDomain(domain);
|
|
30
|
+
|
|
31
|
+
const result: CorsResult = {
|
|
32
|
+
domain, vulnerable: false, findings: [], error: null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
for (const test of CORS_TESTS) {
|
|
37
|
+
const origin = test.origin
|
|
38
|
+
.replace(/TARGET/g, domain);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const resp = await fetch(`https://${domain}`, {
|
|
42
|
+
signal: AbortSignal.timeout(8000),
|
|
43
|
+
headers: {
|
|
44
|
+
"Origin": origin,
|
|
45
|
+
"User-Agent": "DomainSniper/2.0",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const acao = resp.headers.get("access-control-allow-origin");
|
|
50
|
+
const acac = resp.headers.get("access-control-allow-credentials");
|
|
51
|
+
const allowed = acao === origin || acao === "*";
|
|
52
|
+
const credentials = acac === "true";
|
|
53
|
+
|
|
54
|
+
if (allowed) {
|
|
55
|
+
const finding: CorsFinding = {
|
|
56
|
+
test: test.name,
|
|
57
|
+
origin,
|
|
58
|
+
allowed: true,
|
|
59
|
+
credentials,
|
|
60
|
+
severity: credentials ? "critical" : test.severity,
|
|
61
|
+
detail: credentials
|
|
62
|
+
? `Reflects origin ${origin} WITH credentials — full account takeover possible`
|
|
63
|
+
: `Reflects origin ${origin} (no credentials)`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
result.findings.push(finding);
|
|
67
|
+
if (credentials || test.severity === "critical") {
|
|
68
|
+
result.vulnerable = true;
|
|
69
|
+
}
|
|
70
|
+
} else if (acao === "*") {
|
|
71
|
+
result.findings.push({
|
|
72
|
+
test: test.name,
|
|
73
|
+
origin,
|
|
74
|
+
allowed: true,
|
|
75
|
+
credentials: false,
|
|
76
|
+
severity: "medium",
|
|
77
|
+
detail: "Wildcard ACAO (*) — allows any origin to read responses",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Connection error — skip this test
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
} catch (err: unknown) {
|
|
87
|
+
result.error = err instanceof Error ? err.message : "CORS check failed";
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { assertValidDomain } from "../validate.js";
|
|
4
|
+
import type { DnsDetails } from "../types.js";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
async function digQuery(domain: string, type: string): Promise<string[]> {
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await execFileAsync("dig", ["+short", domain, type], { timeout: 10000 });
|
|
11
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function lookupDns(domain: string): Promise<DnsDetails> {
|
|
18
|
+
assertValidDomain(domain);
|
|
19
|
+
const [a, aaaa, mx, txt, cname] = await Promise.all([
|
|
20
|
+
digQuery(domain, "A"),
|
|
21
|
+
digQuery(domain, "AAAA"),
|
|
22
|
+
digQuery(domain, "MX"),
|
|
23
|
+
digQuery(domain, "TXT"),
|
|
24
|
+
digQuery(domain, "CNAME"),
|
|
25
|
+
]);
|
|
26
|
+
return { a, aaaa, mx, txt, cname };
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function calculateDomainAge(createdDate: string | null): string | null {
|
|
2
|
+
if (!createdDate) return null;
|
|
3
|
+
try {
|
|
4
|
+
const created = new Date(createdDate);
|
|
5
|
+
if (isNaN(created.getTime())) return null;
|
|
6
|
+
const now = new Date();
|
|
7
|
+
const diffMs = now.getTime() - created.getTime();
|
|
8
|
+
if (diffMs < 0) return "Not yet created";
|
|
9
|
+
const days = Math.floor(diffMs / 86400000);
|
|
10
|
+
if (days < 1) return "< 1 day";
|
|
11
|
+
if (days < 30) return `${days}d`;
|
|
12
|
+
if (days < 365) {
|
|
13
|
+
const months = Math.floor(days / 30);
|
|
14
|
+
return `${months}mo`;
|
|
15
|
+
}
|
|
16
|
+
const years = Math.floor(days / 365);
|
|
17
|
+
const remainingMonths = Math.floor((days % 365) / 30);
|
|
18
|
+
return remainingMonths > 0 ? `${years}y ${remainingMonths}mo` : `${years}y`;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function daysUntilExpiry(expiryDate: string | null): number | null {
|
|
25
|
+
if (!expiryDate) return null;
|
|
26
|
+
try {
|
|
27
|
+
const expiry = new Date(expiryDate);
|
|
28
|
+
if (isNaN(expiry.getTime())) return null;
|
|
29
|
+
return Math.floor((expiry.getTime() - Date.now()) / 86400000);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const PREFIXES = [
|
|
2
|
+
"get", "try", "use", "go", "my", "hey", "the",
|
|
3
|
+
"super", "hyper", "ultra", "mega", "meta", "neo",
|
|
4
|
+
"re", "un", "co", "ai",
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const SUFFIXES = [
|
|
8
|
+
"app", "hq", "hub", "lab", "labs", "io", "ly",
|
|
9
|
+
"ify", "ize", "ful", "box", "kit", "now",
|
|
10
|
+
"run", "dev", "ops", "ai", "x", "up",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const TECH_WORDS = [
|
|
14
|
+
"sync", "flow", "stack", "link", "dash", "grid",
|
|
15
|
+
"node", "edge", "core", "loop", "ping", "bolt",
|
|
16
|
+
"wave", "spark", "cloud", "beam", "data", "byte",
|
|
17
|
+
"pixel", "craft", "forge", "vault", "pulse", "shift",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export interface Suggestion {
|
|
21
|
+
name: string;
|
|
22
|
+
domain: string;
|
|
23
|
+
strategy: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function generateSuggestions(
|
|
27
|
+
keyword: string,
|
|
28
|
+
tld: string = "com",
|
|
29
|
+
maxResults: number = 30
|
|
30
|
+
): Suggestion[] {
|
|
31
|
+
const word = keyword.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
32
|
+
if (!word) return [];
|
|
33
|
+
|
|
34
|
+
const suggestions: Suggestion[] = [];
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
|
|
37
|
+
function add(name: string, strategy: string) {
|
|
38
|
+
const domain = `${name}.${tld}`;
|
|
39
|
+
if (!seen.has(domain) && name.length >= 3 && name.length <= 20) {
|
|
40
|
+
seen.add(domain);
|
|
41
|
+
suggestions.push({ name, domain, strategy });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Prefix combinations
|
|
46
|
+
for (const prefix of PREFIXES) {
|
|
47
|
+
if (suggestions.length >= maxResults) break;
|
|
48
|
+
add(`${prefix}${word}`, `prefix: ${prefix}+`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Suffix combinations
|
|
52
|
+
for (const suffix of SUFFIXES) {
|
|
53
|
+
if (suggestions.length >= maxResults) break;
|
|
54
|
+
add(`${word}${suffix}`, `suffix: +${suffix}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Word mashups
|
|
58
|
+
for (const tech of TECH_WORDS) {
|
|
59
|
+
if (suggestions.length >= maxResults) break;
|
|
60
|
+
add(`${word}${tech}`, `mashup: +${tech}`);
|
|
61
|
+
add(`${tech}${word}`, `mashup: ${tech}+`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Truncations
|
|
65
|
+
if (word.length > 4) {
|
|
66
|
+
add(word.slice(0, 4), "truncation: first 4");
|
|
67
|
+
add(word.slice(0, 5), "truncation: first 5");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Vowel removal
|
|
71
|
+
const noVowels = word.replace(/[aeiou]/g, "");
|
|
72
|
+
if (noVowels.length >= 3 && noVowels !== word) {
|
|
73
|
+
add(noVowels, "vowel removal");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Double last letter
|
|
77
|
+
add(`${word}${word.slice(-1)}`, "doubled ending");
|
|
78
|
+
|
|
79
|
+
// Rhyme patterns
|
|
80
|
+
const rhymeSuffixes = ["oo", "ee", "ify", "ly", "er", "le"];
|
|
81
|
+
for (const r of rhymeSuffixes) {
|
|
82
|
+
if (suggestions.length >= maxResults) break;
|
|
83
|
+
add(`${word}${r}`, `rhyme: +${r}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return suggestions.slice(0, maxResults);
|
|
87
|
+
}
|