Skip to content

Commit 30a9b2b

Browse files
committed
ct rss feed
1 parent 3374a56 commit 30a9b2b

File tree

6 files changed

+267
-110
lines changed

6 files changed

+267
-110
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { Certificate } from "@/types/certificate";
3+
import client from "@/lib/clickhouse";
4+
import { generateRSSFeed, getRSSResponse, getBaseUrl, RSSItem } from "@/lib/rss";
5+
6+
interface FeedParams {
7+
params: Promise<{ domain: string }>;
8+
}
9+
10+
async function getCTEntriesForDomain(domain: string, limit: number = 50): Promise<Certificate[]> {
11+
const sql = `
12+
SELECT
13+
certificate_sha256,
14+
log_id,
15+
log_index,
16+
entry_timestamp,
17+
not_after,
18+
subject_common_name,
19+
issuer_common_name,
20+
issuer_organization
21+
FROM ct_log_entries_by_name
22+
WHERE name_rev = reverse({domain:String}) OR
23+
name_rev LIKE reverse({wildcard:String})
24+
ORDER BY entry_timestamp DESC
25+
LIMIT {limit:UInt32}
26+
SETTINGS max_execution_time = 5, max_threads = 1, max_memory_usage = 134217728
27+
`;
28+
29+
const resultSet = await client.query({
30+
query: sql,
31+
query_params: {
32+
domain: domain,
33+
wildcard: `%.${domain}`,
34+
limit: limit,
35+
},
36+
format: "JSONEachRow",
37+
});
38+
39+
return await resultSet.json<Certificate>();
40+
}
41+
42+
function createCTRSSItems(entries: Certificate[], domain: string): RSSItem[] {
43+
const baseUrl = getBaseUrl();
44+
45+
return entries.map(entry => {
46+
const pubDate = new Date(entry.entry_timestamp).toUTCString();
47+
const entryUrl = `${baseUrl}/search/${encodeURIComponent(entry.certificate_sha256)}?type=sha256`;
48+
49+
const getTitle = () => {
50+
const cn = entry.subject_common_name || "Unknown CN";
51+
return `CT: ${cn}`;
52+
};
53+
54+
const getDescription = () => {
55+
let desc = `New Certificate Transparency log entry for domain ${domain}`;
56+
desc += `\nSubject CN: ${entry.subject_common_name || 'N/A'}`;
57+
desc += `\nIssuer: ${entry.issuer_common_name || 'N/A'}`;
58+
if (entry.issuer_organization && entry.issuer_organization.length > 0) {
59+
desc += ` (${entry.issuer_organization[0]})`;
60+
}
61+
desc += `\nCertificate SHA256: ${entry.certificate_sha256}`;
62+
desc += `\nLog ID: ${entry.log_id}`;
63+
desc += `\nLog Index: ${entry.log_index}`;
64+
desc += `\nExpires: ${new Date(entry.not_after).toISOString().split('T')[0]}`;
65+
return desc;
66+
};
67+
68+
return {
69+
title: getTitle(),
70+
description: getDescription(),
71+
link: entryUrl,
72+
guid: entryUrl,
73+
pubDate: pubDate,
74+
};
75+
});
76+
}
77+
78+
export async function GET(request: NextRequest, { params }: FeedParams) {
79+
try {
80+
const { domain } = await params;
81+
const decodedDomain = decodeURIComponent(domain);
82+
const { searchParams } = new URL(request.url);
83+
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100);
84+
85+
const entries = await getCTEntriesForDomain(decodedDomain, limit);
86+
const items = createCTRSSItems(entries, decodedDomain);
87+
88+
const baseUrl = getBaseUrl();
89+
const feedUrl = `${baseUrl}/api/ct/feed/${encodeURIComponent(decodedDomain)}`;
90+
const webUrl = `${baseUrl}/search/${encodeURIComponent(decodedDomain)}`;
91+
92+
const rssXml = generateRSSFeed({
93+
title: `Certificate Transparency Entries for ${decodedDomain}`,
94+
description: `Recent Certificate Transparency log entries for domain ${decodedDomain}`,
95+
link: webUrl,
96+
feedUrl: feedUrl,
97+
items: items,
98+
});
99+
100+
const response = getRSSResponse(rssXml);
101+
return new NextResponse(response.body, { headers: response.headers });
102+
} catch (error) {
103+
console.error('CT RSS feed generation error:', error);
104+
return NextResponse.json(
105+
{ error: 'Failed to generate CT RSS feed' },
106+
{ status: 500 }
107+
);
108+
}
109+
}

ui/app/api/sigstore/feed/[org]/[repo]/route.ts

Lines changed: 30 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { SigstoreEntry } from "@/types/sigstore";
33
import client from "@/lib/clickhouse";
4-
5-
function escapeXml(unsafe: string): string {
6-
return unsafe.replace(/[<>&'"]/g, (c) => {
7-
switch (c) {
8-
case '<': return '&lt;';
9-
case '>': return '&gt;';
10-
case '&': return '&amp;';
11-
case '\'': return '&apos;';
12-
case '"': return '&quot;';
13-
default: return c;
14-
}
15-
});
16-
}
4+
import { generateRSSFeed, getRSSResponse, getBaseUrl, RSSItem } from "@/lib/rss";
175

186
interface FeedParams {
197
params: Promise<{ org: string; repo: string }>;
@@ -32,7 +20,7 @@ async function getSigstoreEntriesForRepo(org: string, repo: string, limit: numbe
3220
WHERE repository_name = {repoName:String}
3321
ORDER BY integrated_time DESC
3422
LIMIT {limit:UInt32}
35-
SETTINGS max_execution_time = 30, max_threads = 1, max_memory_usage = 268435456
23+
SETTINGS max_execution_time = 5, max_threads = 1, max_memory_usage = 134217728
3624
`;
3725

3826
const resultSet = await client.query({
@@ -47,15 +35,12 @@ async function getSigstoreEntriesForRepo(org: string, repo: string, limit: numbe
4735
return await resultSet.json<SigstoreEntry>();
4836
}
4937

50-
function generateRSSFeed(entries: SigstoreEntry[], org: string, repo: string): string {
51-
const now = new Date().toUTCString();
52-
const repositoryName = `${org}/${repo}`;
53-
const feedUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'https://transparency.cafe'}/api/sigstore/feed/${org}/${repo}`;
54-
const webUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'https://transparency.cafe'}/sigstore/search/${encodeURIComponent(repositoryName)}?type=github_repository`;
55-
56-
const items = entries.map(entry => {
38+
function createSigstoreRepoRSSItems(entries: SigstoreEntry[], repositoryName: string): RSSItem[] {
39+
const baseUrl = getBaseUrl();
40+
41+
return entries.map(entry => {
5742
const pubDate = new Date(entry.integrated_time).toUTCString();
58-
const entryUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'https://transparency.cafe'}/sigstore/entry/${entry.entry_uuid}`;
43+
const entryUrl = `${baseUrl}/sigstore/entry/${entry.entry_uuid}`;
5944

6045
const getTitle = () => {
6146
return `Rekor: ${repositoryName}`;
@@ -69,30 +54,14 @@ function generateRSSFeed(entries: SigstoreEntry[], org: string, repo: string): s
6954
return desc;
7055
};
7156

72-
return `
73-
<item>
74-
<title>${escapeXml(getTitle())}</title>
75-
<description>${escapeXml(getDescription())}</description>
76-
<link>${escapeXml(entryUrl)}</link>
77-
<guid isPermaLink="true">${escapeXml(entryUrl)}</guid>
78-
<pubDate>${pubDate}</pubDate>
79-
</item>`;
80-
}).join('\n');
81-
82-
return `<?xml version="1.0" encoding="UTF-8"?>
83-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
84-
<channel>
85-
<title>${escapeXml(`Sigstore Rekor Entries for ${repositoryName}`)}</title>
86-
<description>${escapeXml(`Recent Sigstore Rekor transparency log entries for GitHub repository ${repositoryName}`)}</description>
87-
<link>${escapeXml(webUrl)}</link>
88-
<atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml" />
89-
<lastBuildDate>${now}</lastBuildDate>
90-
<generator>transparency.cafe</generator>
91-
<language>en-us</language>
92-
<ttl>60</ttl>
93-
${items}
94-
</channel>
95-
</rss>`;
57+
return {
58+
title: getTitle(),
59+
description: getDescription(),
60+
link: entryUrl,
61+
guid: entryUrl,
62+
pubDate: pubDate,
63+
};
64+
});
9665
}
9766

9867
export async function GET(request: NextRequest, { params }: FeedParams) {
@@ -102,14 +71,23 @@ export async function GET(request: NextRequest, { params }: FeedParams) {
10271
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100);
10372

10473
const entries = await getSigstoreEntriesForRepo(org, repo, limit);
105-
const rssXml = generateRSSFeed(entries, org, repo);
74+
const repositoryName = `${org}/${repo}`;
75+
const items = createSigstoreRepoRSSItems(entries, repositoryName);
76+
77+
const baseUrl = getBaseUrl();
78+
const feedUrl = `${baseUrl}/api/sigstore/feed/${org}/${repo}`;
79+
const webUrl = `${baseUrl}/sigstore/search/${encodeURIComponent(repositoryName)}?type=github_repository`;
10680

107-
return new NextResponse(rssXml, {
108-
headers: {
109-
'Content-Type': 'application/rss+xml; charset=utf-8',
110-
'Cache-Control': 'public, max-age=300, s-maxage=300', // Cache for 5 minutes
111-
},
81+
const rssXml = generateRSSFeed({
82+
title: `Sigstore Rekor Entries for ${repositoryName}`,
83+
description: `Recent Sigstore Rekor transparency log entries for GitHub repository ${repositoryName}`,
84+
link: webUrl,
85+
feedUrl: feedUrl,
86+
items: items,
11287
});
88+
89+
const response = getRSSResponse(rssXml);
90+
return new NextResponse(response.body, { headers: response.headers });
11391
} catch (error) {
11492
console.error('RSS feed generation error:', error);
11593
return NextResponse.json(

ui/app/api/sigstore/feed/[org]/route.ts

Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { SigstoreEntry } from "@/types/sigstore";
33
import client from "@/lib/clickhouse";
4-
5-
function escapeXml(unsafe: string): string {
6-
return unsafe.replace(/[<>&'"]/g, (c) => {
7-
switch (c) {
8-
case '<': return '&lt;';
9-
case '>': return '&gt;';
10-
case '&': return '&amp;';
11-
case '\'': return '&apos;';
12-
case '"': return '&quot;';
13-
default: return c;
14-
}
15-
});
16-
}
4+
import { generateRSSFeed, getRSSResponse, getBaseUrl, RSSItem } from "@/lib/rss";
175

186
interface FeedParams {
197
params: Promise<{ org: string }>;
@@ -31,7 +19,7 @@ async function getSigstoreEntriesForOrg(org: string, limit: number = 50): Promis
3119
WHERE repository_name LIKE {orgPattern:String}
3220
ORDER BY integrated_time DESC
3321
LIMIT {limit:UInt32}
34-
SETTINGS max_execution_time = 30, max_threads = 1, max_memory_usage = 268435456
22+
SETTINGS max_execution_time = 5, max_threads = 1, max_memory_usage = 134217728
3523
`;
3624

3725
const resultSet = await client.query({
@@ -46,14 +34,12 @@ async function getSigstoreEntriesForOrg(org: string, limit: number = 50): Promis
4634
return await resultSet.json<SigstoreEntry>();
4735
}
4836

49-
function generateRSSFeed(entries: SigstoreEntry[], org: string): string {
50-
const now = new Date().toUTCString();
51-
const feedUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'https://transparency.cafe'}/api/sigstore/feed/${org}`;
52-
const webUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'https://transparency.cafe'}/sigstore/search/${encodeURIComponent(org)}?type=github_organization`;
53-
54-
const items = entries.map(entry => {
37+
function createSigstoreOrgRSSItems(entries: SigstoreEntry[]): RSSItem[] {
38+
const baseUrl = getBaseUrl();
39+
40+
return entries.map(entry => {
5541
const pubDate = new Date(entry.integrated_time).toUTCString();
56-
const entryUrl = `${process.env.NEXT_PUBLIC_BASE_URL || 'https://transparency.cafe'}/sigstore/entry/${entry.entry_uuid}`;
42+
const entryUrl = `${baseUrl}/sigstore/entry/${entry.entry_uuid}`;
5743

5844
const getTitle = () => {
5945
const repo = entry.repository_name || "Unknown Repository";
@@ -68,30 +54,14 @@ function generateRSSFeed(entries: SigstoreEntry[], org: string): string {
6854
return desc;
6955
};
7056

71-
return `
72-
<item>
73-
<title>${escapeXml(getTitle())}</title>
74-
<description>${escapeXml(getDescription())}</description>
75-
<link>${escapeXml(entryUrl)}</link>
76-
<guid isPermaLink="true">${escapeXml(entryUrl)}</guid>
77-
<pubDate>${pubDate}</pubDate>
78-
</item>`;
79-
}).join('\n');
80-
81-
return `<?xml version="1.0" encoding="UTF-8"?>
82-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
83-
<channel>
84-
<title>${escapeXml(`Sigstore Rekor Entries for ${org}`)}</title>
85-
<description>${escapeXml(`Recent Sigstore Rekor transparency log entries for GitHub organization ${org}`)}</description>
86-
<link>${escapeXml(webUrl)}</link>
87-
<atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml" />
88-
<lastBuildDate>${now}</lastBuildDate>
89-
<generator>transparency.cafe</generator>
90-
<language>en-us</language>
91-
<ttl>60</ttl>
92-
${items}
93-
</channel>
94-
</rss>`;
57+
return {
58+
title: getTitle(),
59+
description: getDescription(),
60+
link: entryUrl,
61+
guid: entryUrl,
62+
pubDate: pubDate,
63+
};
64+
});
9565
}
9666

9767
export async function GET(request: NextRequest, { params }: FeedParams) {
@@ -101,14 +71,22 @@ export async function GET(request: NextRequest, { params }: FeedParams) {
10171
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100);
10272

10373
const entries = await getSigstoreEntriesForOrg(org, limit);
104-
const rssXml = generateRSSFeed(entries, org);
74+
const items = createSigstoreOrgRSSItems(entries);
75+
76+
const baseUrl = getBaseUrl();
77+
const feedUrl = `${baseUrl}/api/sigstore/feed/${org}`;
78+
const webUrl = `${baseUrl}/sigstore/search/${encodeURIComponent(org)}?type=github_organization`;
10579

106-
return new NextResponse(rssXml, {
107-
headers: {
108-
'Content-Type': 'application/rss+xml; charset=utf-8',
109-
'Cache-Control': 'public, max-age=300, s-maxage=300', // Cache for 5 minutes
110-
},
80+
const rssXml = generateRSSFeed({
81+
title: `Sigstore Rekor Entries for ${org}`,
82+
description: `Recent Sigstore Rekor transparency log entries for GitHub organization ${org}`,
83+
link: webUrl,
84+
feedUrl: feedUrl,
85+
items: items,
11186
});
87+
88+
const response = getRSSResponse(rssXml);
89+
return new NextResponse(response.body, { headers: response.headers });
11290
} catch (error) {
11391
console.error('RSS feed generation error:', error);
11492
return NextResponse.json(

ui/app/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const dynamic = "force-dynamic";
66
export default function Home() {
77
return (
88
<div className="min-h-full">
9-
<div className="container px-6 pt-6 max-w-4xl">
9+
<div className="container px-6 pt-6 pb-12 max-w-4xl">
1010
<div className="space-y-8">
1111
<div className="text-sm flex flex-col gap-2">
1212
<p>
@@ -21,6 +21,14 @@ export default function Home() {
2121
</div>
2222

2323
<SearchForm />
24+
<div>
25+
<p className="mb-4 font-bold">RSS Feed</p>
26+
<p>
27+
<code className="text-sm">
28+
https://transparency.cafe/api/ct/feed/[domain]
29+
</code>
30+
</p>
31+
</div>
2432
<CtStats />
2533
</div>
2634
</div>

0 commit comments

Comments
 (0)