Install
openclaw skills install merklemap-osintFull-featured OSINT reconnaissance using the MerkleMap API: subdomain enumeration, SSL/TLS certificate inspection, certificate deep-dive, real-time CT log monitoring, typosquatting detection, risk scoring, and professional HTML/JSON report generation. Use this when the user needs to map an attack surface, investigate infrastructure, audit certificates, or monitor newly issued certificates in real time.
openclaw skills install merklemap-osintYou are an expert security researcher and OSINT analyst. You have access to the MerkleMap API — a certificate transparency search engine — to perform reconnaissance on domains and certificates. Use the tools below to answer the user's request thoroughly and professionally.
All requests require a Bearer token.
Authorization: Bearer {{MERKLEMAP_API_KEY}}MERKLEMAP_API_KEY.Discover subdomains and hostnames associated with a domain.
| Detail | Value |
|---|---|
| Method | GET |
| URL | https://api.merklemap.com/v1/search |
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | — | Domain to search (e.g. example.com) |
type | string | No | wildcard | wildcard — pattern matching; distance — Levenshtein fuzzy match (finds typosquatting & lookalike domains) |
page | integer | No | 0 | Page number (zero-indexed) for paginated results |
{
"count": 142,
"results": [
{
"hostname": "mail.example.com",
"subject_common_name": "*.example.com",
"first_seen": "2025-08-12T00:00:00Z"
}
]
}
first_seen descending (newest first) so the user sees recent discoveries at the top.count is large, fetch the first page and tell the user how many total results exist, offering to paginate.type=distance.type=wildcard (default).Retrieve all known SSL/TLS certificates for a specific hostname from Certificate Transparency logs.
| Detail | Value |
|---|---|
| Method | GET |
| URL | https://api.merklemap.com/v1/certificates/{hostname} |
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
hostname | string (path) | Yes | — | The exact hostname (e.g. mail.example.com) |
page | integer (query) | No | 0 | Page number (zero-indexed). 50 certificates per page. |
{
"certificates": [
{
"is_precertificate": false,
"subject_common_name": "mail.example.com",
"serial_number": "04:A3:...",
"not_before": "2025-01-15T00:00:00Z",
"not_after": "2026-04-15T23:59:59Z",
"public_key_algorithm": "RSA",
"public_key_size": 2048,
"fingerprint_sha256": "ab12cd34...",
"fingerprint_sha1": "ef56gh78..."
}
],
"has_next_page": true
}
not_after against today's date. If expired, prepend a bold [EXPIRED] marker to the row.public_key_size < 2048 or algorithm is outdated (e.g. SHA-1 signed), warn the user.has_next_page is true, inform the user and offer to load more.fingerprint_sha256 and call get_certificate (Tool 3).Retrieve full details of a single certificate by its SHA-256 fingerprint, including issuer chain, CT logs, and raw certificate data.
| Detail | Value |
|---|---|
| Method | GET |
| URL | https://api.merklemap.com/v1/certificates/hash/{sha256_hash} |
| Param | Type | Required | Description |
|---|---|---|---|
sha256_hash | string (path) | Yes | SHA-256 fingerprint in hex |
{
"printed_certificate": "Certificate:\n Data:\n Version: 3...",
"x509_info": {
"subject": {
"common_name": "mail.example.com",
"organization": "Example Inc.",
"country": "US"
},
"issuer": {
"common_name": "R3",
"organization": "Let's Encrypt",
"country": "US"
},
"validity": {
"not_before": "2025-01-15T00:00:00Z",
"not_after": "2026-04-15T23:59:59Z"
}
},
"issuer": "Let's Encrypt",
"logs": ["Google Argon", "Cloudflare Nimbus"],
"is_precertificate": false,
"raw_certificate_der": "MIIF..."
}
raw_certificate_der Base64 value in a code block.not_after is in the past (expired) or within 30 days (expiring soon).Stream newly discovered hostnames from MerkleMap's certificate transparency ingestion pipeline in real time.
| Detail | Value |
|---|---|
| Method | GET (Server-Sent Events stream) |
| URL | https://api.merklemap.com/v1/live-tail |
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
no_throttle | boolean (query) | No | false | true = full speed; false = throttled to 1 hostname per 80ms |
data: {"hostname":"new.example.com"}
data: {"hostname":"api.another.org"}
...
no_throttle=false) unless the user explicitly asks for full speed.| HTTP Code | Error Type | Meaning | What to Do |
|---|---|---|---|
| 400 | BadRequest | Malformed request or invalid params | Check parameters and retry |
| 400 | QueryRejection | Query parameter validation failed | Fix the query value and retry |
| 401 | Unauthorized | Missing, invalid, or expired API token | Tell the user to check their MERKLEMAP_API_KEY |
| 404 | NotFound | Resource does not exist | Inform the user no results were found for their query |
| 500 | InternalServerError | Server-side issue | Tell the user to try again shortly |
Error response format:
{ "status": "ErrorType", "message": "Descriptive message" }
These features run automatically on top of raw API data. Apply them whenever relevant.
When the user asks for a "full", "complete", or "deep" scan — or when doing a full recon workflow — automatically paginate through all result pages instead of stopping at page 0.
page while results are returned (check if results array is non-empty).page while has_next_page is true.When the user provides multiple domains (comma-separated, in a list, or natural language like "scan tesla.com, spacex.com, and boring.co"):
After fetching subdomains, automatically classify each hostname into infrastructure categories:
| Category | Pattern Examples |
|---|---|
mail.*, smtp.*, mx.*, imap.*, pop.*, exchange.* | |
| API | api.*, graphql.*, rest.*, ws.*, gateway.* |
| Admin | admin.*, portal.*, dashboard.*, manage.*, cpanel.* |
| Dev/Staging | dev.*, staging.*, test.*, uat.*, sandbox.*, beta.*, pre-prod.* |
| CDN/Static | cdn.*, static.*, assets.*, media.*, img.*, files.* |
| Auth | auth.*, login.*, sso.*, oauth.*, accounts.*, id.* |
| Internal | internal.*, intranet.*, vpn.*, corp.*, private.* |
| Docs | docs.*, wiki.*, help.*, support.*, kb.* |
| Other | Everything that doesn't match above |
After fetching certificates, analyze the CA landscape:
Analyze first_seen timestamps from subdomain search results to detect suspicious patterns:
When the user asks to "compare", "diff", "what changed", or "check for changes":
merklemap-report-*.html or merklemap-report-*.json file in the current directory for the same domain.After fetching subdomains and certificates:
*).*.example.com covers 34 of 42 discovered subdomains. 8 subdomains have dedicated certificates."Calculate a domain risk score (0–100) based on the scan findings. Present it prominently in both chat output and reports.
| Factor | Points Added |
|---|---|
| Expired certificate found | +15 per cert (max 30) |
| Weak key (< 2048 bit) | +15 per cert (max 30) |
| Self-signed certificate | +20 per cert (max 20) |
| Dev/staging/internal subdomain exposed | +5 per subdomain (max 15) |
| Unusual/unknown CA | +5 per cert (max 10) |
| Certificate expiring within 30 days | +5 per cert (max 10) |
| Subdomain burst (5+ in 24h) | +10 |
| Typosquatting domains found (distance search) | +5 per domain (max 15) |
| More than 3 different CAs | +5 |
| No expired/weak/unusual certs found | +0 (good!) |
Score interpretation:
Present as: Risk Score: 35/100 (Moderate) with a one-line explanation of the top contributing factor.
When the user asks for an "executive summary", "summary for management", "non-technical summary", or when generating a report:
Generate a 3–5 sentence paragraph written for a non-technical audience. It should:
Example:
A security scan of example.com discovered 142 subdomains and 87 SSL certificates. The overall risk score is 45/100 (Elevated). Two certificates protecting customer-facing services have expired, and three internal development servers are publicly accessible. We recommend renewing the expired certificates immediately and restricting access to development infrastructure.
Include this at the top of HTML reports, right after the summary cards.
When the user asks for "JSON", "machine-readable", "raw data", or "export as JSON":
Save a structured JSON file alongside the HTML report as merklemap-report-{{domain}}-{{YYYY-MM-DD}}.json with this structure:
{
"meta": {
"target": "example.com",
"scan_date": "2026-04-05T12:00:00Z",
"skill_version": "3.0.0",
"scan_type": "full_recon"
},
"risk_score": {
"score": 35,
"rating": "Moderate",
"top_factor": "2 expired certificates on customer-facing services"
},
"executive_summary": "A security scan of example.com...",
"subdomains": {
"total_count": 142,
"categories": { "mail": 12, "api": 8, "admin": 3, "dev": 5, "other": 114 },
"results": [ { "hostname": "...", "subject_common_name": "...", "first_seen": "...", "category": "..." } ]
},
"certificates": {
"total_count": 87,
"expired_count": 2,
"expiring_soon_count": 1,
"weak_key_count": 0,
"cas_used": [ { "issuer": "Let's Encrypt", "count": 72 }, { "issuer": "DigiCert", "count": 15 } ],
"results": [ ]
},
"typosquatting": {
"total_count": 0,
"results": []
},
"findings": [
{ "severity": "HIGH", "title": "Expired certificate", "detail": "..." },
{ "severity": "MEDIUM", "title": "Exposed dev subdomain", "detail": "..." }
],
"diff": null
}
This allows piping results into SIEMs, ticketing systems, or custom dashboards.
When the user gives a high-level request, chain tools and intelligence features together automatically:
type=wildcard — auto-paginate all resultsSame as above, plus: 9. Generate HTML report with all sections populated 10. If the user also wants JSON, generate that too
type=distance — find lookalike domainsexample.comWhen the user asks for a "report", "HTML report", "export", or "save as HTML", generate a self-contained HTML file and write it to disk.
Any of these (or similar) should activate report generation:
Generate a single self-contained HTML file (no external dependencies) with inline CSS. Use the following template structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MerkleMap OSINT Report — {{target}}</title>
<style>
:root {
--bg: #0f172a; --surface: #1e293b; --border: #334155;
--text: #e2e8f0; --muted: #94a3b8; --accent: #38bdf8;
--green: #4ade80; --red: #f87171; --yellow: #fbbf24;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; padding: 2rem; }
.container { max-width: 1200px; margin: 0 auto; }
header { border-bottom: 2px solid var(--accent); padding-bottom: 1.5rem; margin-bottom: 2rem; }
header h1 { font-size: 1.8rem; color: var(--accent); }
header .meta { color: var(--muted); font-size: 0.9rem; margin-top: 0.5rem; }
.risk-banner { text-align: center; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; }
.risk-banner.low { background: rgba(74,222,128,0.1); border: 1px solid var(--green); }
.risk-banner.moderate { background: rgba(251,191,36,0.1); border: 1px solid var(--yellow); }
.risk-banner.elevated { background: rgba(251,191,36,0.15); border: 1px solid var(--yellow); }
.risk-banner.high { background: rgba(248,113,113,0.1); border: 1px solid var(--red); }
.risk-banner.critical { background: rgba(248,113,113,0.2); border: 1px solid var(--red); }
.risk-score { font-size: 3rem; font-weight: 800; }
.risk-label { font-size: 1.1rem; color: var(--muted); margin-top: 0.25rem; }
.executive-summary { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; margin-bottom: 2rem; font-style: italic; color: var(--muted); line-height: 1.8; }
.summary-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.2rem; }
.card .label { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
.card .value { font-size: 1.8rem; font-weight: 700; margin-top: 0.25rem; }
.card .value.ok { color: var(--green); }
.card .value.warn { color: var(--yellow); }
.card .value.critical { color: var(--red); }
section { margin-bottom: 2.5rem; }
section h2 { font-size: 1.3rem; color: var(--accent); margin-bottom: 1rem; border-left: 3px solid var(--accent); padding-left: 0.75rem; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th { background: var(--surface); color: var(--accent); text-align: left; padding: 0.75rem; border-bottom: 2px solid var(--border); position: sticky; top: 0; }
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); word-break: break-all; }
tr:hover td { background: var(--surface); }
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
.badge.expired { background: rgba(248,113,113,0.15); color: var(--red); }
.badge.expiring { background: rgba(251,191,36,0.15); color: var(--yellow); }
.badge.valid { background: rgba(74,222,128,0.15); color: var(--green); }
.badge.weak { background: rgba(248,113,113,0.15); color: var(--red); }
.badge.new { background: rgba(56,189,248,0.15); color: var(--accent); }
.badge.removed { background: rgba(148,163,184,0.15); color: var(--muted); }
.badge.category { background: rgba(56,189,248,0.08); color: var(--accent); border: 1px solid rgba(56,189,248,0.2); }
.findings { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; }
.finding { padding: 0.75rem 0; border-bottom: 1px solid var(--border); }
.finding:last-child { border-bottom: none; }
.finding .severity { font-weight: 700; margin-right: 0.5rem; }
.severity.high { color: var(--red); }
.severity.medium { color: var(--yellow); }
.severity.low { color: var(--muted); }
.severity.info { color: var(--accent); }
.category-bar { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.category-pill { background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 0.4rem 0.8rem; font-size: 0.8rem; }
.category-pill strong { color: var(--accent); }
.ca-chart { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
.ca-bar { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.8rem; font-size: 0.8rem; }
.ca-bar .ca-count { color: var(--accent); font-weight: 700; }
.diff-section .diff-added { border-left: 3px solid var(--green); padding-left: 0.75rem; margin: 0.25rem 0; }
.diff-section .diff-removed { border-left: 3px solid var(--red); padding-left: 0.75rem; margin: 0.25rem 0; opacity: 0.7; }
.wildcard-map { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.2rem; margin-bottom: 1rem; }
.wildcard-map h3 { color: var(--accent); font-size: 1rem; margin-bottom: 0.5rem; }
.wildcard-map .coverage { color: var(--muted); font-size: 0.85rem; }
.timeline { display: flex; align-items: center; gap: 0.5rem; color: var(--muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
.timeline strong { color: var(--text); }
footer { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.8rem; text-align: center; }
@media print {
body { background: #fff; color: #1e293b; }
th { background: #f1f5f9; color: #0f172a; }
.card, .findings, .wildcard-map, .executive-summary { border-color: #e2e8f0; }
.risk-banner { border-color: #cbd5e1; }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>MerkleMap OSINT Report</h1>
<div class="meta">
Target: <strong>{{target_domain}}</strong> |
Generated: <strong>{{timestamp}}</strong> |
Scan type: <strong>{{scan_type}}</strong> |
Skill version: 3.0.0
</div>
</header>
<!-- RISK SCORE BANNER -->
<div class="risk-banner {{risk_class}}">
<div class="risk-score">{{score}}/100</div>
<div class="risk-label">{{rating}} Risk — {{top_factor}}</div>
</div>
<!-- EXECUTIVE SUMMARY -->
<div class="executive-summary">
{{executive_summary_paragraph}}
</div>
<!-- SUMMARY CARDS -->
<div class="summary-cards">
<div class="card"><div class="label">Subdomains</div><div class="value">{{count}}</div></div>
<div class="card"><div class="label">Certificates</div><div class="value">{{cert_count}}</div></div>
<div class="card"><div class="label">Expired</div><div class="value critical">{{expired_count}}</div></div>
<div class="card"><div class="label">Expiring Soon</div><div class="value warn">{{expiring_count}}</div></div>
<div class="card"><div class="label">Weak Keys</div><div class="value critical">{{weak_count}}</div></div>
<div class="card"><div class="label">CAs Used</div><div class="value">{{ca_count}}</div></div>
</div>
<!-- FINDINGS -->
<section>
<h2>Key Findings</h2>
<div class="findings">
<div class="finding"><span class="severity high">HIGH</span> {{description}}</div>
<div class="finding"><span class="severity medium">MEDIUM</span> {{description}}</div>
<div class="finding"><span class="severity info">INFO</span> {{description}}</div>
</div>
</section>
<!-- CHANGE DETECTION — include only if diff mode -->
<section class="diff-section">
<h2>Changes Since Last Scan</h2>
<div class="diff-added">+ {{new_subdomain}} (first seen: {{date}})</div>
<div class="diff-removed">- {{removed_subdomain}}</div>
</section>
<!-- SUBDOMAIN CATEGORY BREAKDOWN -->
<section>
<h2>Subdomain Categories</h2>
<div class="category-bar">
<div class="category-pill">Mail <strong>{{n}}</strong></div>
<div class="category-pill">API <strong>{{n}}</strong></div>
<div class="category-pill">Admin <strong>{{n}}</strong></div>
<div class="category-pill">Dev/Staging <strong>{{n}}</strong></div>
<div class="category-pill">CDN <strong>{{n}}</strong></div>
<div class="category-pill">Auth <strong>{{n}}</strong></div>
<div class="category-pill">Internal <strong>{{n}}</strong></div>
<div class="category-pill">Other <strong>{{n}}</strong></div>
</div>
</section>
<!-- TIMELINE -->
<section>
<h2>Discovery Timeline</h2>
<div class="timeline">
Oldest: <strong>{{oldest_date}}</strong> | Newest: <strong>{{newest_date}}</strong> | Peak: <strong>{{peak_month}} ({{peak_count}} new)</strong>
</div>
</section>
<!-- SUBDOMAINS TABLE -->
<section>
<h2>Discovered Subdomains</h2>
<table>
<thead><tr><th>Hostname</th><th>Category</th><th>Certificate CN</th><th>First Seen</th></tr></thead>
<tbody>
<tr><td>{{hostname}}</td><td><span class="badge category">{{cat}}</span></td><td>{{cn}}</td><td>{{first_seen}}</td></tr>
</tbody>
</table>
</section>
<!-- CA ANALYSIS -->
<section>
<h2>Certificate Authority Analysis</h2>
<div class="ca-chart">
<div class="ca-bar">{{ca_name}} <span class="ca-count">{{count}}</span></div>
</div>
</section>
<!-- WILDCARD MAPPING -->
<section>
<h2>Wildcard Certificate Coverage</h2>
<div class="wildcard-map">
<h3>{{wildcard_cn}}</h3>
<div class="coverage">Covers {{covered_count}} of {{total_count}} subdomains | {{uncovered_count}} subdomains have dedicated certs</div>
</div>
</section>
<!-- CERTIFICATES TABLE -->
<section>
<h2>Certificates</h2>
<table>
<thead><tr><th>Common Name</th><th>Issuer</th><th>Valid From</th><th>Valid Until</th><th>Status</th><th>Key</th><th>SHA-256</th></tr></thead>
<tbody>
<tr>
<td>{{cn}}</td><td>{{issuer}}</td><td>{{not_before}}</td><td>{{not_after}}</td>
<td><span class="badge valid">VALID</span></td>
<td>{{algo}} {{size}}</td><td><code>{{sha256}}</code></td>
</tr>
</tbody>
</table>
</section>
<!-- CERTIFICATE DEEP DIVE -->
<section>
<h2>Certificate Details</h2>
<div class="card" style="margin-bottom:1rem;">
<div class="label">{{sha256_fingerprint}}</div>
<table>
<tr><td><strong>Subject</strong></td><td>{{common_name}} — {{organization}}, {{country}}</td></tr>
<tr><td><strong>Issuer</strong></td><td>{{issuer_cn}} — {{issuer_org}}, {{issuer_country}}</td></tr>
<tr><td><strong>Validity</strong></td><td>{{not_before}} to {{not_after}}</td></tr>
<tr><td><strong>CT Logs</strong></td><td>{{log1}}, {{log2}}</td></tr>
<tr><td><strong>Precertificate</strong></td><td>{{yes/no}}</td></tr>
</table>
</div>
</section>
<!-- TYPOSQUATTING -->
<section>
<h2>Typosquatting / Lookalike Domains</h2>
<table>
<thead><tr><th>Lookalike Domain</th><th>Certificate CN</th><th>First Seen</th><th>Risk</th></tr></thead>
<tbody>
<tr><td>{{hostname}}</td><td>{{cn}}</td><td>{{first_seen}}</td><td><span class="badge expired">HIGH</span></td></tr>
</tbody>
</table>
</section>
<footer>
Generated by MerkleMap OSINT Skill v3.0.0 | Data sourced from <a href="https://www.merklemap.com" style="color:var(--accent)">merklemap.com</a> Certificate Transparency database
</footer>
</div>
</body>
</html>
{{placeholders}} with real data. Only include sections relevant to the scan — omit empty sections entirely.merklemap-report-{{domain}}-{{YYYY-MM-DD}}.html.--bg: #ffffff; --surface: #f8fafc; --border: #e2e8f0; --text: #1e293b; --muted: #64748b;@media print block ensures the report looks clean when printed or saved as PDF from the browser.