diff --git a/docs/cert.html b/docs/cert.html new file mode 100644 index 0000000..96e1fea --- /dev/null +++ b/docs/cert.html @@ -0,0 +1,69 @@ + + +
+Leaf X.509 certificate fetching and CA/Browser Forum validation-level +classification from policy OIDs. Source: internal/cert/cert.go.
+Reserved policy OIDs: 2.23.140.1.1 (EV), +2.23.140.1.2.1 (DV), 2.23.140.1.2.2 (OV), +2.23.140.1.2.3 (IV). Priority order is EV > OV > IV > DV.
+ +type Details struct {
+ Issuer string // Organization, or fallback issuer string
+ Class string // EV, OV, IV, DV, or Unknown
+ SANs []string // DNS SANs (CN fallback), empty when unavailable
+ OIDs []string // raw policy OIDs found
+ Subject string
+ IssuerStr string
+ NotBefore time.Time
+ NotAfter time.Time
+ HasCert bool
+} |
Extracted leaf-certificate fields. Issuer prefers the +Organization attribute and falls back to Common Name, then the full +issuer string. SANs falls back to the subject Common Name.
+ +func Classify(oids []string) string |
Maps a set of policy OID strings to a validation class using the ordered +priority table (EV first). Returns "Unknown" when none match.
+cert.Classify([]string{"2.23.140.1.2.1"}) // => "DV" |
func InspectDomain(ctx context.Context, domain string, perCall time.Duration) (*Details, error) |
Dials domain:443 with tls.Dialer +(InsecureSkipVerify, since the peer is untrusted by definition) and +returns the parsed leaf via FromLeaf. +Transient failures are retried up to 3 times with a 1 second delay.
+| Parameter | Description |
|---|---|
| ctx | Parent context; the global 15 second budget. |
| domain | Already-normalized domain to dial. |
| perCall | Per-attempt deadline (5 seconds in the CLI). |
func FromLeaf(leaf *x509.Certificate) *Details |
Pure constructor: builds Details from an +already-parsed leaf certificate. Preferred entry point for tests.
+ +func (d *Details) SANsString() string |
Joins SANs with ", " for display, or returns +"Unknown" when empty. Nil-receiver safe.
+ + diff --git a/docs/dns.html b/docs/dns.html new file mode 100644 index 0000000..521baff --- /dev/null +++ b/docs/dns.html @@ -0,0 +1,43 @@ + + + +Domain normalization, syntax validation, and DNS pre-flight resolution +checks. Source: internal/dns/dns.go.
+ +func NormalizeDomain(d string) string |
Lowercases the input and strips scheme, path, port, and trailing dot. +Mirrors the shell normalize_domain helper.
+dns.NormalizeDomain("HTTPS://Example.COM:8443/path")
+// => "example.com" |
func ValidSyntax(d string) bool |
Reports whether d looks like a dotted domain name: labels of +alphanumerics and hyphens with at least one dot. Used to reject malformed +input before any network call.
+dns.ValidSyntax("jdsoft.com") // => true
+dns.ValidSyntax("no-dot") // => false |
func CheckDNS(ctx context.Context, domain string) error |
Aborts early on NXDOMAIN or unresolvable names via +net.DefaultResolver.LookupIP with a 5 second deadline. Returns +nil when at least one IPv4 address resolves, an error otherwise.
+| Parameter | Description |
|---|---|
| ctx | Parent context; the global 15 second budget. |
| domain | Already-normalized domain to resolve. |
CLI entry point and fetch orchestration. Source: +cmd/isthislegit/main.go. Package main exposes no exported +symbols; this page documents behavior.
+ +Syntax: isthislegit [FLAGS] <legit_domain> +<suspect_domain>. Flags -v/--verbose, +-j/--json, --scale, -h/--help, plus a +-- separator. Unknown flags and anything other than exactly two +positional domains exit 1 with a usage message. Domains are normalized with +dns.NormalizeDomain and validated +with dns.ValidSyntax.
+ +| Budget | Value |
|---|---|
| Global deadline (both domains) | 15 seconds |
| Per-call deadline (DNS, TLS, RDAP) | 5 seconds |
| Attempts per call | 3, with 1 second between retries |
Current version string is 1.01, printed in the banner, JSON +output, and --scale header. Build target: +CGO_ENABLED=0 go build -ldflags="-s -w" -o isthislegit +./cmd/isthislegit.
+ + diff --git a/docs/nav.html b/docs/nav.html new file mode 100644 index 0000000..a7e398f --- /dev/null +++ b/docs/nav.html @@ -0,0 +1,63 @@ + + + +Packages
+ +dns
+ +cert
+ +rdap
+ +score
+ +report
+ + + diff --git a/docs/overview.html b/docs/overview.html new file mode 100644 index 0000000..1b65618 --- /dev/null +++ b/docs/overview.html @@ -0,0 +1,71 @@ + + + +Domain Impersonation and OSINT Triage Tool. Compares a known-legitimate +domain (target) against a suspect domain using DNS resolution, X.509 TLS +certificates, and RDAP registration data, then produces a deterministic risk +score and verdict. Single static binary; standard library plus +golang.org/x/sync only.
+ +git clone https://github.com/example/isthislegit.git +cd isthislegit +CGO_ENABLED=0 go build -ldflags="-s -w" -o isthislegit ./cmd/isthislegit +./isthislegit jdsoft.com jdsoftcareers.com +go test ./... |
| Flag | Description |
|---|---|
| -v, --verbose | Append raw cert policy OIDs, TLS details, and RDAP JSON. |
| -j, --json | Output the result summary as JSON for scripting. |
| --scale | Print the scoring scale table and exit. |
| -h, --help | Display usage and exit. |
Both domains are fetched concurrently with +golang.org/x/sync/errgroup under a 15 second global deadline; each +DNS, TLS, and RDAP call carries a 5 second deadline and is retried up to 3 +times. Total run time equals the slower of the two domains.
+parse args --> DNS pre-flight (target + suspect, parallel) + --> fetch target = RDAP + TLS in parallel + --> fetch suspect = RDAP + TLS in parallel + --> fail-closed verify --> score --> render |
Exit codes: 0 on success (including --scale and +--help), 1 on bad flags, invalid domains, DNS failure, or +failed data-quality verification.
+ +| Signal | Points |
|---|---|
| Suspect domain age < 30 days (CRITICAL) | +80 |
| Suspect domain age 30-89 days (WARNING) | +80 |
| Cert downgrade: target EV/OV vs suspect DV | +40 |
| Registrar mismatch | +30 |
| Same-class issuer difference (display only) | +0 |
| Score | Verdict |
|---|---|
| 0-14 | LIKELY LEGITIMATE |
| 15-39 | LIKELY UNRELATED / MISCONFIGURED |
| 40-50 | SUSPICIOUS - MANUAL REVIEW RECOMMENDED |
| 51+ | LIKELY PHISHING / IMPERSONATION ATTEMPT |
RDAP registration-data client over https://rdap.org/domain/. +Source: internal/rdap/rdap.go.
+ +type Event struct {
+ Action string `json:"eventAction"`
+ Date string `json:"eventDate"`
+} |
Single RDAP event object; only registration actions are used.
+ +type Response struct {
+ Events []Event `json:"events"`
+ Entities []entity `json:"entities"`
+} |
Minimal parsed RDAP payload. Entities carry registrar roles and vCard +arrays used for registrar extraction.
+ +type Result struct {
+ Raw []byte // raw JSON, kept for --verbose output
+ Resp *Response // parsed payload
+ Found bool // false when the lookup failed
+} |
func Query(ctx context.Context, client *http.Client, domain string, perCall time.Duration) (*Result, error) |
Fetches RDAP JSON for domain with an Accept: +application/json request, retrying transient failures up to 3 times. +Non-2xx status and invalid JSON are errors.
+| Parameter | Description |
|---|---|
| ctx | Parent context; the global 15 second budget. |
| client | Shared *http.Client; per-call deadline via context. |
| domain | Already-normalized domain to look up. |
| perCall | Per-attempt deadline (5 seconds in the CLI). |
func RegistrationDate(r *Result) string |
Returns the YYYY-MM-DD date of the registration event, or +"Unknown / Lookup Failed" when unavailable. Nil-safe.
+ +func Registrar(r *Result) string |
Best-effort registrar name, or "Unknown". Prefers entities with +the registrar role, falling back to the first vCard +fn property. Nil-safe.
+ + diff --git a/docs/report.html b/docs/report.html new file mode 100644 index 0000000..c8d1d89 --- /dev/null +++ b/docs/report.html @@ -0,0 +1,64 @@ + + + +Human, JSON, scale, and verbose renderers. Source: +internal/report/report.go.
+ +type Data struct {
+ Version string
+ Target score.DomainData
+ Suspect score.DomainData
+ Score int
+ Findings []string
+ Verdict string
+ AgeFlag string
+ TargetCert *cert.Details
+ SuspectCert *cert.Details
+ TargetRDAP *rdap.Result
+ SuspectRDAP *rdap.Result
+ TargetDomain string
+ SuspectDomain string
+} |
Everything a renderer needs: scored domain data plus the raw cert and +RDAP results backing verbose output.
+ +func Scale(version string) string |
Static scoring-scale reference table (no lookups). Backs the +--scale flag.
+ +func Human(d Data) string |
Triage report with per-domain blocks (creation date, registrar, issuer, +validation, SANs), the scored risk analysis, and the verdict. Empty +findings render a clean bill of health.
+ +func JSON(d Data) string |
Machine-readable summary with tool, version, +target, suspect (including suspect age_days), +risk_score, findings, and verdict. Ends with a +newline for pipeline use.
+ +func Verbose(d Data) string |
Raw policy OIDs with classes, per-domain TLS details (issuer, subject, +validity, SANs), and raw RDAP JSON for both domains. Missing data renders +an explicit unavailable notice per section.
+ + diff --git a/docs/score.html b/docs/score.html new file mode 100644 index 0000000..87e41c0 --- /dev/null +++ b/docs/score.html @@ -0,0 +1,88 @@ + + + +Deterministic risk scoring: domain-age check (+80), certificate +downgrade check (+40), and registrar mismatch (+30), mapped onto a +four-tier verdict. Sources: internal/score/score.go, +internal/score/date.go.
+ +type DomainData struct {
+ Domain string // queried domain
+ Date string // YYYY-MM-DD, or sentinel on failure
+ Registrar string // "Unknown" when unavailable
+ Issuer string // "Unknown" when unavailable
+ Class string // EV, OV, IV, DV, or Unknown
+ SANs string // comma-joined, or "Unknown"
+ AgeDays int
+ AgeKnown bool
+} |
Collected triage fields for one domain; the scoring input.
+ +type Result struct {
+ Score int
+ Findings []string
+ Verdict string
+ AgeFlag string // display suffix for young suspect domains
+} |
func ClassLong(c string) string |
Human-readable validation name via lookup table: EV to +"Extended Validation", OV to "Organization Validated", DV +to "Domain Validated", IV to "Individual Validated", +anything else to "Unknown".
+ +func VerifyDataQuality(target, suspect DomainData) error |
Fails closed: returns an error when either domain lacks a valid RDAP +date, a computable age, or any TLS signal (issuer and class both unknown). +The CLI aborts instead of printing a verdict built on missing data.
+ +func Score(target, suspect DomainData) Result |
Applies the three signals. Suspect age under 30 days adds 80 with a +CRITICAL flag; 30-89 days adds 80 with a WARNING flag. Target EV/OV versus +suspect DV adds 40 as DOWNGRADE; a same-class issuer difference is a +display-only NOTE (+0). Case-insensitive registrar mismatch adds 30, +skipped when either side is unknown.
+r := score.Score(target, suspect) +// r.Score, r.Findings, r.Verdict, r.AgeFlag |
func Verdict(score int) string |
Maps the total via threshold table: 51+ phishing, 40+ suspicious, 15+ +unrelated/misconfigured, otherwise legitimate.
+ +func ValidISODate(s string) bool |
True only for clean, calendar-valid YYYY-MM-DD strings. Any +sentinel or garbage value fails by design.
+ +func AgeDays(ymd string, now time.Time) (days int, ok bool) |
Whole days between ymd and now; ok is false +when ymd is unparseable.
+ +