Changelog - Recent Changes & Updates

Track the evolution of this website through its commit history—see how features and improvements have been added over time.

feat

Published
Author
entr0phy4
David A.

implement changelog page to display commit history

@@ -0,0 +1,122 @@
+import { type Metadata } from 'next'
+import Image from 'next/image'
+import { Octokit } from 'octokit'
+
+import { Border } from '@/components/Border'
+import { Container } from '@/components/Container'
+import { FadeIn } from '@/components/FadeIn'
+import { PageIntro } from '@/components/PageIntro'
+import { formatDate } from '@/lib/formatDate'
+import { RenderTags } from '@/lib/renderTags'
+import Link from 'next/link'
+
+export const metadata: Metadata = {
+ title: 'Changelog',
+ description: 'Changelog',
+}
+
+export default async function Changelog() {
+ const octokit = new Octokit({
+ auth: process.env.GH_TOKEN,
+ })
+
+ const { data: commits } = await octokit.request('GET /repos/{owner}/{repo}/commits', {
+ owner: 'entr0phy4',
+ repo: 'entr0phy4.github.io',
+ headers: {
+ 'X-GitHub-Api-Version': '2022-11-28'
+ },
+ })
+
+ const commitsWithDetails = await Promise.all(
+ commits.map(async (commit) => {
+ const { data: details } = await octokit.request('GET /repos/{owner}/{repo}/commits/{ref}', {
+ owner: 'entr0phy4',
+ repo: 'entr0phy4.github.io',
+ ref: commit.sha,
+ headers: {
+ 'X-GitHub-Api-Version': '2022-11-28'
+ },
+ })
+ return details
+ })
+ )
+
+ return (
+ <>
+ <PageIntro eyebrow="Changelog" title="Recent Changes & Updates">
+ <p>Track the evolution of this website through its commit history—see how features and improvements have been added over time.</p>
+ </PageIntro>
+
+ <Container className="mt-24 sm:mt-32 lg:mt-40">
+ <div>
+ {commitsWithDetails.map((commit, index) => (
+ <FadeIn key={index}>
+ <article>
+ <Border className="pt-16">
+ <div className="relative lg:-mx-4 lg:flex lg:justify-end">
+ <div className="pt-10 lg:w-2/3 lg:flex-none lg:px-4 lg:pt-0">
+ <p className="mt-6 max-w-2xl text-base text-neutral-600">
+ <RenderTags tags={[commit.commit?.message?.split(':')[0] || '']} />
+ </p>
+ <dl className="lg:absolute lg:top-0 lg:left-0 lg:w-1/3 lg:px-4">
+ <dt className="sr-only">Published</dt>
+ <dd className="absolute top-0 left-0 text-sm text-[#00ff00] lg:static">
+ <time dateTime={commit.commit?.author?.date?.split('T')[0]}>
+ {formatDate(commit.commit?.author?.date?.split('T')[0] || '')}
+ </time>
+ </dd>
+
+ <dt className="sr-only">Author</dt>
+ <dd className="mt-6 flex gap-x-4">
+ <div className="flex-none overflow-hidden rounded-xl bg-black">
+ <Image
+ alt=""
+ width={48}
+ height={48}
+ src={commit.committer?.avatar_url || ''}
+ className="h-12 w-12 object-cover grayscale"
+ />
+ </div>
+ <div className="mb-4 text-sm text-white">
+ <div className="font-semibold">
+ {commit.author?.login}
+ </div>
+ <div>{commit.commit?.author?.name}</div>
+ </div>
+ </dd>
+ </dl>
+
+ <h2 className="font-display text-2xl font-semibold text-white">
+ <Link href={commit.html_url || ''}>{commit.commit?.message?.split(':')[1]}</Link>
+ </h2>
+
+ <div className="mt-4 text-sm text-neutral-400">
+ {commit.files?.[0]?.patch?.split('\n').map((line, i) => (
+ <div
+ key={i}
+ className={`font-mono text-sm py-0.5 ${
+ line.startsWith('+')
+ ? 'text-[#00ff00] bg-[#002200]'
+ : line.startsWith('-')
+ ? 'text-[#ff0000] bg-[#220000]'
+ : 'text-neutral-400'
+ }`}
+ >
+ <code className="px-2">{line}</code>
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ </Border>
+ </article>
+ </FadeIn>
+ ))}
+ </div>
+ </Container>
+
+ {/* <ContactSection /> */}
+ </>
+ )
+}

deps

Published
Author
entr0phy4
David A.

add octokit package

@@ -26,6 +26,7 @@
"framer-motion": "^10.15.2",
"mdx-annotations": "^0.1.4",
"next": "^14.0.4",
+ "octokit": "^4.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recma-import-images": "0.0.3",
@@ -1176,6 +1177,347 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@octokit/app": {
+ "version": "15.1.6",
+ "resolved": "https://registry.npmjs.org/@octokit/app/-/app-15.1.6.tgz",
+ "integrity": "sha512-WELCamoCJo9SN0lf3SWZccf68CF0sBNPQuLYmZ/n87p5qvBJDe9aBtr5dHkh7T9nxWZ608pizwsUbypSzZAiUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-app": "^7.2.1",
+ "@octokit/auth-unauthenticated": "^6.1.3",
+ "@octokit/core": "^6.1.5",
+ "@octokit/oauth-app": "^7.1.6",
+ "@octokit/plugin-paginate-rest": "^12.0.0",
+ "@octokit/types": "^14.0.0",
+ "@octokit/webhooks": "^13.6.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/auth-app": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.2.1.tgz",
+ "integrity": "sha512-4jaopCVOtWN0V8qCx/1s2pkRqC6tcvIQM3kFB99eIpsP53GfsoIKO08D94b83n/V3iGihHmxWR2lXzE0NicUGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-app": "^8.1.4",
+ "@octokit/auth-oauth-user": "^5.1.4",
+ "@octokit/request": "^9.2.3",
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0",
+ "toad-cache": "^3.7.0",
+ "universal-github-app-jwt": "^2.2.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app": {
+ "version": "8.1.4",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.4.tgz",
+ "integrity": "sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^7.1.5",
+ "@octokit/auth-oauth-user": "^5.1.4",
+ "@octokit/request": "^9.2.3",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.5.tgz",
+ "integrity": "sha512-lR00+k7+N6xeECj0JuXeULQ2TSBB/zjTAmNF2+vyGPDEFx1dgk1hTDmL13MjbSmzusuAmuJD8Pu39rjp9jH6yw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-methods": "^5.1.5",
+ "@octokit/request": "^9.2.3",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.4.tgz",
+ "integrity": "sha512-4tJRofMHm6ZCd3O2PVgboBbQ/lNtacREeaihet0+wCATZmvPK+jjg2K6NjBfY69An3yzQdmkcMeiaOOoxOPr7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^7.1.5",
+ "@octokit/oauth-methods": "^5.1.5",
+ "@octokit/request": "^9.2.3",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
+ "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/auth-unauthenticated": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-6.1.3.tgz",
+ "integrity": "sha512-d5gWJla3WdSl1yjbfMpET+hUSFCE15qM0KVSB0H1shyuJihf/RL1KqWoZMIaonHvlNojkL9XtLFp8QeLe+1iwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "6.1.5",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
+ "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^5.0.0",
+ "@octokit/graphql": "^8.2.2",
+ "@octokit/request": "^9.2.3",
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0",
+ "before-after-hook": "^3.0.2",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "10.1.4",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
+ "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz",
+ "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^9.2.3",
+ "@octokit/types": "^14.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/oauth-app": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-7.1.6.tgz",
+ "integrity": "sha512-OMcMzY2WFARg80oJNFwWbY51TBUfLH4JGTy119cqiDawSFXSIBujxmpXiKbGWQlvfn0CxE6f7/+c6+Kr5hI2YA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-app": "^8.1.3",
+ "@octokit/auth-oauth-user": "^5.1.3",
+ "@octokit/auth-unauthenticated": "^6.1.2",
+ "@octokit/core": "^6.1.4",
+ "@octokit/oauth-authorization-url": "^7.1.1",
+ "@octokit/oauth-methods": "^5.1.4",
+ "@types/aws-lambda": "^8.10.83",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/oauth-authorization-url": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz",
+ "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/oauth-methods": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.5.tgz",
+ "integrity": "sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-authorization-url": "^7.0.0",
+ "@octokit/request": "^9.2.3",
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz",
+ "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/openapi-webhooks-types": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-11.0.0.tgz",
+ "integrity": "sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-paginate-graphql": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.4.tgz",
+ "integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-12.0.0.tgz",
+ "integrity": "sha512-MPd6WK1VtZ52lFrgZ0R2FlaoiWllzgqFHaSZxvp72NmoDeZ0m8GeJdg4oB6ctqMTYyrnDYp592Xma21mrgiyDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-14.0.0.tgz",
+ "integrity": "sha512-iQt6ovem4b7zZYZQtdv+PwgbL5VPq37th1m2x2TdkgimIDJpsi2A6Q/OI/23i/hR6z5mL0EgisNR4dcbmckSZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-retry": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.2.1.tgz",
+ "integrity": "sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0",
+ "bottleneck": "^2.15.3"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-throttling": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-10.0.0.tgz",
+ "integrity": "sha512-Kuq5/qs0DVYTHZuBAzCZStCzo2nKvVRo/TDNhCcpC2TKiOGz/DisXMCvjt3/b5kr6SCI1Y8eeeJTHBxxpFvZEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0",
+ "bottleneck": "^2.15.3"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": "^6.1.3"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "9.2.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
+ "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^10.1.4",
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0",
+ "fast-content-type-parse": "^2.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "6.1.8",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
+ "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz",
+ "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^25.0.0"
+ }
+ },
+ "node_modules/@octokit/webhooks": {
+ "version": "13.8.2",
+ "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-13.8.2.tgz",
+ "integrity": "sha512-NgKpxIVp9MnEukvTF9s871v11bYTDZwpuyIYkU0ZIEM3d+DBOeiciIJPuCdjFMC1pQgv05MNKtdhbl8+Q+i3GA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-webhooks-types": "11.0.0",
+ "@octokit/request-error": "^6.1.7",
+ "@octokit/webhooks-methods": "^5.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/webhooks-methods": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-5.1.1.tgz",
+ "integrity": "sha512-NGlEHZDseJTCj8TMMFehzwa9g7On4KJMPVHDSrHxCQumL6uSQR8wIkP/qesv52fXqV1BPf4pTxwtS31ldAt9Xg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1635,6 +1977,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/aws-lambda": {
+ "version": "8.10.149",
+ "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz",
+ "integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==",
+ "license": "MIT"
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -2530,12 +2878,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/before-after-hook": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
+ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
+ "license": "Apache-2.0"
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
+ "node_modules/bottleneck": {
+ "version": "2.19.5",
+ "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
+ "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -3847,6 +4207,22 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
+ "node_modules/fast-content-type-parse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
+ "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -7104,6 +7480,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/octokit": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/octokit/-/octokit-4.1.3.tgz",
+ "integrity": "sha512-PP+EL8h4xPCE9NBo6jXq6I2/EiTXsn1cg9F0IZehHBv/qhuQpyGMFElEB17miWKciuT6vRHiFFiG9+FoXOmg6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/app": "^15.1.6",
+ "@octokit/core": "^6.1.5",
+ "@octokit/oauth-app": "^7.1.6",
+ "@octokit/plugin-paginate-graphql": "^5.2.4",
+ "@octokit/plugin-paginate-rest": "^12.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "^14.0.0",
+ "@octokit/plugin-retry": "^7.2.1",
+ "@octokit/plugin-throttling": "^10.0.0",
+ "@octokit/request-error": "^6.1.8",
+ "@octokit/types": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -8790,6 +9187,15 @@
"node": ">=8.0"
}
},
+ "node_modules/toad-cache": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
+ "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -9214,6 +9620,18 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/universal-github-app-jwt": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz",
+ "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==",
+ "license": "MIT"
+ },
+ "node_modules/universal-user-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+ "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
+ "license": "ISC"
+ },
"node_modules/unrs-resolver": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz",

style

Published
Author
entr0phy4
David A.

update padding for tag elements

@@ -8,7 +8,7 @@ export function RenderTags({ tags }: { tags: string[] }) {
}
return tags.map((tag, index) => (
- <span className="px-1 text-white" key={index}>
+ <span className="pr-1 text-white" key={index}>
<Badge color={getRandomColorClass()}>{tag}</Badge>
</span>
))

ci

Published
Author
entr0phy4
David A.

add GH_TOKEN environment variable to build job

@@ -28,6 +28,8 @@ jobs:
# Build job
build:
runs-on: ubuntu-latest
+ env:
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4

feat

Published
Author
entr0phy4
David A.

render tags on main page

@@ -20,6 +20,7 @@ import logoPhobiaLight from '@/images/clients/phobia/logo-light.svg'
import logoUnseal from '@/images/clients/unseal/logo-light.svg'
import imageLaptop from '@/images/laptop.jpg'
import { type CaseStudy, type MDXEntry, loadCaseStudies } from '@/lib/mdx'
+import { RenderTags } from '@/lib/renderTags'
const clients = [
['Phobia', logoPhobiaLight],
@@ -190,11 +191,36 @@ export default async function Home() {
Software engineer from <strong>Colombia</strong>.
</p>
</FadeIn>
+ <FadeIn className="mt-2">
+ <dd>
+ <RenderTags
+ tags={[
+ 'Hacking',
+ 'Linux',
+ 'Software',
+ 'Hardware',
+ 'Backend',
+ 'Hack The Box',
+ 'Development',
+ 'Frontend',
+ ]}
+ />
+ </dd>
+ </FadeIn>
</Container>
+ <SectionIntro
+ title="Mind is a maze of twisty little passages, all alike."
+ className="mt-24 sm:mt-32 lg:mt-40"
+ >
+ <p className="text-right font-thin italic">
+ - Colossal Cave Adventure - 1976
+ </p>
+ </SectionIntro>
+
{/* <Clients /> */}
- <CaseStudies caseStudies={caseStudies} />
+ {/* <CaseStudies caseStudies={caseStudies} />
{/* <Testimonial */}
{/* className="mt-24 sm:mt-32 lg:mt-40" */}
@@ -204,7 +230,7 @@ export default async function Home() {
{/* finding a way to access the user’s microphone without triggering one of */}
{/* those annoying permission dialogs. */}
{/* </Testimonial> */}
- {/**/}
+
{/* <Services /> */}
{/* <ContactSection /> */}

chore

Published
Author
entr0phy4
David A.

add missing word

@@ -46,7 +46,7 @@ target = {
### Enumeration {{ id: 'enumeration' }}
-With the first [Nmap](https://nmap.org/) we try to find out the open port of the target:
+With the first [Nmap](https://nmap.org/) scan we try to find out the open port of the target:
```bash
$ nmap -p- -vvv -Pn -n --min-rate 5000 10.10.10.208

style

Published
Author
entr0phy4
David A.

decrease shadow

@@ -149,7 +149,7 @@ export function NavBar({ sections }: Props) {
)}
</Popover>
<FadeIn>
- <div className="hidden shadow-xl sm:flex sm:h-32 sm:justify-center sm:border-b sm:border-white/10 sm:shadow-white/5 sm:[@supports(backdrop-filter:blur(0))]:bg-transparent sm:[@supports(backdrop-filter:blur(0))]:backdrop-blur-sm">
+ <div className="hidden shadow-sm sm:flex sm:h-32 sm:justify-center sm:border-b sm:border-white/10 sm:shadow-white/5 sm:[@supports(backdrop-filter:blur(0))]:bg-transparent sm:[@supports(backdrop-filter:blur(0))]:backdrop-blur-sm">
<ol
role="list"
className="mb-[-2px] grid auto-cols-[minmax(0,15rem)] grid-flow-col text-base font-medium text-white [counter-reset:section]"

style

Published
Author
entr0phy4
David A.

add lateral padding to tags

@@ -8,7 +8,7 @@ export function RenderTags({ tags }: { tags: string[] }) {
}
return tags.map((tag, index) => (
- <span className="text-white" key={index}>
+ <span className="px-1 text-white" key={index}>
<Badge color={getRandomColorClass()}>{tag}</Badge>
</span>
))

refactor

Published
Author
entr0phy4
David A.

render tags extracted

@@ -10,11 +10,12 @@ import { FadeIn } from '@/components/FadeIn'
import { PageIntro } from '@/components/PageIntro'
import { formatDate } from '@/lib/formatDate'
import { loadArticles } from '@/lib/mdx'
+import { Badge, type Colors, colors } from '@/components/Badge'
+import { RenderTags } from '@/lib/renderTags'
export const metadata: Metadata = {
title: 'Blog',
- description:
- 'Stay up-to-date with the latest industry news as our marketing teams finds new ways to re-purpose old CSS tricks articles.',
+ description: '',
}
export default async function Blog() {
@@ -47,22 +48,28 @@ export default async function Blog() {
{formatDate(article.date)}
</time>
</dd>
+
<dt className="sr-only">Author</dt>
- {/* <dd className="mt-6 flex gap-x-4"> */}
- {/* <div className="flex-none overflow-hidden rounded-xl bg-neutral-100"> */}
- {/* <Image */}
- {/* alt="" */}
- {/* {...article.author.image} */}
- {/* className="h-12 w-12 object-cover grayscale" */}
- {/* /> */}
- {/* </div> */}
- {/* <div className="text-sm text-white"> */}
- {/* <div className="font-semibold"> */}
- {/* {article.author.name} */}
- {/* </div> */}
- {/* <div>{article.author.role}</div> */}
- {/* </div> */}
- {/* </dd> */}
+ <dd className="mt-6 flex gap-x-4">
+ {/* <div className="flex-none overflow-hidden rounded-xl bg-neutral-100"> */}
+ {/* <Image */}
+ {/* alt="" */}
+ {/* {...article.author.image} */}
+ {/* className="h-12 w-12 object-cover grayscale" */}
+ {/* /> */}
+ {/* </div> */}
+ <div className="mb-4 text-sm text-white">
+ <div className="font-semibold">
+ {article.author.name}
+ </div>
+ <div>{article.author.role}</div>
+ </div>
+ </dd>
+ <FadeIn>
+ <dd>
+ <RenderTags tags={article.tags} />
+ </dd>
+ </FadeIn>
</dl>
<p className="mt-6 max-w-2xl text-base text-neutral-600">
{article.description}

feat

Published
Author
entr0phy4
David A.

add tags

@@ -1,6 +1,16 @@
export const article = {
date: '2025-05-04',
title: 'Machine: CrossFit',
+ tags: [
+ 'XSS Injection',
+ 'CSRF bypass',
+ 'Linux',
+ 'Cracking hashes',
+ 'User pivoting',
+ 'Binary analysis',
+ 'FTP enumeration',
+ 'Abuse seed rand',
+ ],
description:
'CrossFit is an insane difficulty Linux box featuring an Apache server that hosts the website of a fictional',
author: {
@@ -19,7 +29,6 @@ export const sections = [
export const metadata = {
title: article.title,
description: article.description,
-
}
## Crossfit
@@ -1052,7 +1061,7 @@ isaac@crossfit:/tmp$ echo -n "$(./get_time)"
1590912600
```
-With this and the ID `9999` we set for the message with our ssh key, we can compute the value `random_value_based_in_current_time + ID_of_message` to later obtain the md5 hash.
+With this and the ID `1234` we set for the message with our ssh key, we can compute the value `random_value_based_in_current_time + ID_of_message` to later obtain the md5 hash.
```bash
isaac@crossfit:/tmp$ echo -n "$(./get_time)1234" | md5sum

feat

Published
Author
entr0phy4
David A.

add custom link

@@ -0,0 +1,14 @@
+import * as Headless from '@headlessui/react'
+import React, { forwardRef } from 'react'
+import NextLink, { type LinkProps } from 'next/link'
+
+export const Link = forwardRef(function Link(
+ props: LinkProps & React.ComponentPropsWithoutRef<'a'>,
+ ref: React.ForwardedRef<HTMLAnchorElement>,
+) {
+ return (
+ <Headless.DataInteractive>
+ <NextLink {...props} ref={ref} />
+ </Headless.DataInteractive>
+ )
+})

feat

Published
Author
entr0phy4
David A.

add badge

@@ -0,0 +1,110 @@
+import * as Headless from '@headlessui/react'
+import clsx from 'clsx'
+import React, { forwardRef } from 'react'
+import { TouchTarget } from './Button'
+import { Link } from './Link'
+
+export const colors = {
+ red: 'bg-red-500/15 text-red-700 group-data-hover:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-hover:bg-red-500/20',
+ orange:
+ 'bg-orange-500/15 text-orange-700 group-data-hover:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-hover:bg-orange-500/20',
+ amber:
+ 'bg-amber-400/20 text-amber-700 group-data-hover:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-hover:bg-amber-400/15',
+ yellow:
+ 'bg-yellow-400/20 text-yellow-700 group-data-hover:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-hover:bg-yellow-400/15',
+ lime: 'bg-lime-400/20 text-lime-700 group-data-hover:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-hover:bg-lime-400/15',
+ green:
+ 'bg-green-500/15 text-green-700 group-data-hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-hover:bg-green-500/20',
+ emerald:
+ 'bg-emerald-500/15 text-emerald-700 group-data-hover:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-hover:bg-emerald-500/20',
+ teal: 'bg-teal-500/15 text-teal-700 group-data-hover:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-hover:bg-teal-500/20',
+ cyan: 'bg-cyan-400/20 text-cyan-700 group-data-hover:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-hover:bg-cyan-400/15',
+ sky: 'bg-sky-500/15 text-sky-700 group-data-hover:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-hover:bg-sky-500/20',
+ blue: 'bg-blue-500/15 text-blue-700 group-data-hover:bg-blue-500/25 dark:text-blue-400 dark:group-data-hover:bg-blue-500/25',
+ indigo:
+ 'bg-indigo-500/15 text-indigo-700 group-data-hover:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-hover:bg-indigo-500/20',
+ violet:
+ 'bg-violet-500/15 text-violet-700 group-data-hover:bg-violet-500/25 dark:text-violet-400 dark:group-data-hover:bg-violet-500/20',
+ purple:
+ 'bg-purple-500/15 text-purple-700 group-data-hover:bg-purple-500/25 dark:text-purple-400 dark:group-data-hover:bg-purple-500/20',
+ fuchsia:
+ 'bg-fuchsia-400/15 text-fuchsia-700 group-data-hover:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-hover:bg-fuchsia-400/20',
+ pink: 'bg-pink-400/15 text-pink-700 group-data-hover:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-hover:bg-pink-400/20',
+ rose: 'bg-rose-400/15 text-rose-700 group-data-hover:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-hover:bg-rose-400/20',
+ zinc: 'bg-zinc-600/10 text-zinc-700 group-data-hover:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-hover:bg-white/10',
+}
+
+export type Colors =
+ | 'red'
+ | 'orange'
+ | 'amber'
+ | 'yellow'
+ | 'lime'
+ | 'green'
+ | 'emerald'
+ | 'teal'
+ | 'cyan'
+ | 'sky'
+ | 'blue'
+ | 'indigo'
+ | 'violet'
+ | 'purple'
+ | 'fuchsia'
+ | 'pink'
+ | 'rose'
+ | 'zinc'
+ | undefined
+type BadgeProps = { color?: Colors }
+
+export function Badge({
+ color = 'zinc',
+ className,
+ ...props
+}: BadgeProps & React.ComponentPropsWithoutRef<'span'>) {
+ return (
+ <span
+ {...props}
+ className={clsx(
+ className,
+ 'inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5 forced-colors:outline',
+ colors[color],
+ )}
+ />
+ )
+}
+
+export const BadgeButton = forwardRef(function BadgeButton(
+ {
+ color = 'zinc',
+ className,
+ children,
+ ...props
+ }: BadgeProps & { className?: string; children: React.ReactNode } & (
+ | Omit<Headless.ButtonProps, 'as' | 'className'>
+ | Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>
+ ),
+ ref: React.ForwardedRef<HTMLElement>,
+) {
+ let classes = clsx(
+ className,
+ 'group relative inline-flex rounded-md focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500',
+ )
+
+ return 'href' in props ? (
+ <Link
+ {...props}
+ className={classes}
+ ref={ref as React.ForwardedRef<HTMLAnchorElement>}
+ >
+ <TouchTarget>
+ <Badge color={color}>{children}</Badge>
+ </TouchTarget>
+ </Link>
+ ) : (
+ <Headless.Button {...props} className={classes} ref={ref}>
+ <TouchTarget>
+ <Badge color={color}>{children}</Badge>
+ </TouchTarget>
+ </Headless.Button>
+ )
+})

feat

Published
Author
entr0phy4
David A.

add touch target

@@ -8,6 +8,18 @@ type ButtonProps = {
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
)
+export function TouchTarget({ children }: { children: React.ReactNode }) {
+ return (
+ <>
+ <span
+ className="absolute top-1/2 left-1/2 size-[max(100%,2.75rem)] -translate-x-1/2 -translate-y-1/2 [@media(pointer:fine)]:hidden"
+ aria-hidden="true"
+ />
+ {children}
+ </>
+ )
+}
+
export function Button({
invert = false,
className,

style

Published
Author
entr0phy4
David A.

change palette colors of mobile popover

@@ -86,43 +86,45 @@ export function NavBar({ sections }: Props) {
<Popover className="sm:hidden">
{({ open }) => (
<>
- <div
- className={clsx(
- 'relative flex items-center px-4 py-3',
- !open &&
- 'bg-white/95 shadow-sm [@supports(backdrop-filter:blur(0))]:bg-white/80 [@supports(backdrop-filter:blur(0))]:backdrop-blur-sm',
- )}
- >
- {!open && (
- <>
- <span
- aria-hidden="true"
- className="font-mono text-sm text-[#00ff00]"
- >
- {(mobileActiveIndex + 1).toString().padStart(2, '0')}
- </span>
- <span className="ml-4 text-base font-medium text-slate-900">
- {sections[mobileActiveIndex].title}
- </span>
- </>
- )}
- <PopoverButton
+ <FadeIn>
+ <div
className={clsx(
- '-mr-1 ml-auto flex h-8 w-8 items-center justify-center',
- open && 'relative z-10',
+ 'relative flex items-center px-4 py-3',
+ !open &&
+ 'bg-white/95 shadow-sm [@supports(backdrop-filter:blur(0))]:bg-black/30 [@supports(backdrop-filter:blur(0))]:backdrop-blur-sm',
)}
- aria-label="Toggle navigation menu"
>
{!open && (
<>
- {/* Increase hit area */}
- <span className="absolute inset-0" />
+ <span
+ aria-hidden="true"
+ className="font-mono text-sm text-[#00ff00]"
+ >
+ {(mobileActiveIndex + 1).toString().padStart(2, '0')}
+ </span>
+ <span className="ml-4 text-base font-medium text-white">
+ {sections[mobileActiveIndex].title}
+ </span>
</>
)}
- <MenuIcon open={open} className="h-6 w-6 stroke-slate-700" />
- </PopoverButton>
- </div>
- <PopoverPanel className="absolute inset-x-0 top-0 bg-white/95 py-3.5 shadow-sm [@supports(backdrop-filter:blur(0))]:bg-white/80 [@supports(backdrop-filter:blur(0))]:backdrop-blur-sm">
+ <PopoverButton
+ className={clsx(
+ '-mr-1 ml-auto flex h-8 w-8 items-center justify-center',
+ open && 'relative z-10',
+ )}
+ aria-label="Toggle navigation menu"
+ >
+ {!open && (
+ <>
+ {/* Increase hit area */}
+ <span className="absolute inset-0" />
+ </>
+ )}
+ <MenuIcon open={open} className="h-6 w-6 stroke-[#00ff00]" />
+ </PopoverButton>
+ </div>
+ </FadeIn>
+ <PopoverPanel className="absolute inset-x-0 top-0 left-0 py-3.5 shadow-sm [@supports(backdrop-filter:blur(0))]:bg-black/30 [@supports(backdrop-filter:blur(0))]:backdrop-blur-sm">
{sections.map((section, sectionIndex) => (
<PopoverButton
as="a"
@@ -132,22 +134,22 @@ export function NavBar({ sections }: Props) {
>
<span
aria-hidden="true"
- className="font-mono text-sm text-blue-600"
+ className="font-mono text-sm text-white"
>
{(sectionIndex + 1).toString().padStart(2, '0')}
</span>
- <span className="ml-4 text-base font-medium text-slate-900">
+ <span className="ml-4 text-base font-medium text-neutral-500">
{section.title}
</span>
</PopoverButton>
))}
</PopoverPanel>
- <div className="absolute inset-x-0 bottom-full z-10 h-4 bg-white" />
+ <div className="absolute inset-x-0 bottom-full z-10 h-4" />
</>
)}
</Popover>
<FadeIn>
- <div className="hidden sm:flex sm:h-32 sm:justify-center sm:border-b sm:border-neutral-900 sm:[@supports(backdrop-filter:blur(0))]:bg-transparent sm:[@supports(backdrop-filter:blur(0))]:backdrop-blur-sm">
+ <div className="hidden shadow-xl sm:flex sm:h-32 sm:justify-center sm:border-b sm:border-white/10 sm:shadow-white/5 sm:[@supports(backdrop-filter:blur(0))]:bg-transparent sm:[@supports(backdrop-filter:blur(0))]:backdrop-blur-sm">
<ol
role="list"
className="mb-[-2px] grid auto-cols-[minmax(0,15rem)] grid-flow-col text-base font-medium text-white [counter-reset:section]"

remove

Published
Author
entr0phy4
David A.

loadSection usless function

@@ -23,28 +23,15 @@ async function loadEntries<T extends { date: string }>(
).sort((a, b) => b.date.localeCompare(a.date))
}
+type ImagePropsWithOptionalAlt = Omit<ImageProps, 'alt'> & { alt?: string }
+
+export type MDXEntry<T> = T & { href: string; metadata: T }
+
export interface Section {
id: string
title: string
}
-export async function loadSections(post: string) {
- const arrPromises = await Promise.all(
- (await glob('**/page.mdx', { cwd: `src/app/blog/${post}` })).map(
- async (filename) => {
- let metadata = await import(`../app/blog/${post}/${filename}`)
- return metadata.sections
- },
- ),
- )
-
- return arrPromises[0]
-}
-
-type ImagePropsWithOptionalAlt = Omit<ImageProps, 'alt'> & { alt?: string }
-
-export type MDXEntry<T> = T & { href: string; metadata: T }
-
export interface Article {
date: string
title: string

feat

Published
Author
entr0phy4
David A.

add sections for treatment tty post

@@ -8,18 +8,29 @@ export const article = {
},
}
+export const sections = [
+ { id: 'introduction', title: 'Introduction' },
+ { id: 'why-we-need-it', title: 'Necessity' },
+ { id: 'steps-to-upgrade', title: 'Steps' },
+ { id: 'benefits', title: 'Benefits' },
+]
+
export const metadata = {
title: article.title,
description: article.description,
}
-### **Introduction**
+---
+
+### **Introduction** {{ id: 'introduction' }}
After compromising a system, one of the first goals is often to upgrade your shell to a fully interactive TTY session. A simple shell, such as those provided by a reverse shell or a basic command execution, may not offer all the functionality needed for an effective post-exploitation scenario. Upgrading to a fully interactive terminal allows attackers or penetration testers to regain control over their session and effectively interact with the compromised system, just as they would in a local terminal environment.
This article covers the **step-by-step process** to achieve this upgrade and describes the necessary commands and techniques to turn a simple shell into a fully interactive TTY, commonly done after establishing a foothold in a vulnerable system.
-### **The Need for an Interactive TTY Shell**
+---
+
+### **The Need for an Interactive TTY Shell** {{ id: 'why-we-need-it'}}
After exploiting a vulnerability and gaining access to a compromised system, the shell might be limited in its functionality. Common limitations include:
@@ -29,7 +40,9 @@ After exploiting a vulnerability and gaining access to a compromised system, the
By upgrading to a fully interactive TTY session, we address these limitations and allow for smoother, more effective interaction with the system.
-### **Steps to Upgrade to an Interactive TTY**
+---
+
+### **Steps to Upgrade to an Interactive TTY** {{ id: 'steps-to-upgrade'}}
**1. Create a Pseudo-Terminal Using `script` (Optional)**
@@ -82,7 +95,7 @@ fg # Bring the process back to the foreground
At this point, you should have a fully interactive terminal session, allowing for advanced features such as editing input, running background jobs, and using terminal shortcuts like Ctrl+C and Ctrl+Z.
-### Post-Exploitation Benefits of an Interactive TTY
+### Post-Exploitation Benefits of an Interactive TTY {{ id: 'benefits' }}
Once the shell is upgraded to an interactive TTY, the attacker or penetration tester gains several advantages:

feat

Published
Author
entr0phy4
David A.

navbar recives sections for each mdx post

@@ -7,20 +7,21 @@ import { PageLinks } from '@/components/PageLinks'
import { formatDate } from '@/lib/formatDate'
import {
type Article,
+ type Section,
type MDXEntry,
loadArticles,
- loadSections,
} from '@/lib/mdx'
export default async function BlogArticleWrapper({
article,
+ sections,
children,
}: {
article: MDXEntry<Article>
+ sections: MDXEntry<Section[]>
children: React.ReactNode
}) {
let allArticles = await loadArticles()
- let allSections = await loadSections('CrossFit')
let moreArticles = allArticles
.filter(({ metadata }) => metadata !== article)
.slice(0, 2)
@@ -44,11 +45,13 @@ export default async function BlogArticleWrapper({
</p>
</header>
</FadeIn>
+ </Container>
- <div className="h-11" />
+ <div className="h-11" />
- <NavBar sections={allSections} />
+ <NavBar sections={sections} />
+ <Container as="article" className="mt-24 sm:mt-32 lg:mt-40">
<FadeIn>
<MDXComponents.wrapper className="mt-24 sm:mt-32 lg:mt-40">
{children}

config

Published
Author
entr0phy4
David A.

add section prop for wrapper blog

@@ -23,14 +23,19 @@ const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
}
-function remarkMDXLayout(source, metaName) {
+function remarkMDXLayout(source, props = {}) {
let parser = Parser.extend(jsx())
let parseOptions = { ecmaVersion: 'latest', sourceType: 'module' }
return (tree) => {
let imp = `import _Layout from '${source}'`
+
+ let propsString = Object.entries(props)
+ .map(([key, value]) => `${key}={${value}}`)
+ .join(' ')
+
let exp = `export default function Layout(props) {
- return <_Layout {...props} ${metaName}={${metaName}} />
+ return <_Layout {...props} ${propsString} />
}`
tree.children.push(
@@ -76,7 +81,13 @@ export default async function config() {
unifiedConditional,
[
new RegExp(`^${escapeStringRegexp(path.resolve('src/app/blog'))}`),
- [[remarkMDXLayout, '@/app/blog/wrapper', 'article']],
+ [
+ [
+ remarkMDXLayout,
+ '@/app/blog/wrapper',
+ { article: 'article', sections: 'sections' },
+ ],
+ ],
],
[
new RegExp(`^${escapeStringRegexp(path.resolve('src/app/work'))}`),

fix

Published
Author
entr0phy4
David A.

remove incorrect import

@@ -4,7 +4,6 @@ import { FadeIn } from '@/components/FadeIn'
import { MDXComponents } from '@/components/MDXComponents'
import { NavBar } from '@/components/NavBar'
import { PageLinks } from '@/components/PageLinks'
-import { TableOfContents } from '@/components/TableOfContent'
import { formatDate } from '@/lib/formatDate'
import {
type Article,

feat

Published
Author
entr0phy4
David A.

add navbar for sections in posts

@@ -2,9 +2,16 @@ import { ContactSection } from '@/components/ContactSection'
import { Container } from '@/components/Container'
import { FadeIn } from '@/components/FadeIn'
import { MDXComponents } from '@/components/MDXComponents'
+import { NavBar } from '@/components/NavBar'
import { PageLinks } from '@/components/PageLinks'
+import { TableOfContents } from '@/components/TableOfContent'
import { formatDate } from '@/lib/formatDate'
-import { type Article, type MDXEntry, loadArticles } from '@/lib/mdx'
+import {
+ type Article,
+ type MDXEntry,
+ loadArticles,
+ loadSections,
+} from '@/lib/mdx'
export default async function BlogArticleWrapper({
article,
@@ -14,6 +21,7 @@ export default async function BlogArticleWrapper({
children: React.ReactNode
}) {
let allArticles = await loadArticles()
+ let allSections = await loadSections('CrossFit')
let moreArticles = allArticles
.filter(({ metadata }) => metadata !== article)
.slice(0, 2)
@@ -38,6 +46,10 @@ export default async function BlogArticleWrapper({
</header>
</FadeIn>
+ <div className="h-11" />
+
+ <NavBar sections={allSections} />
+
<FadeIn>
<MDXComponents.wrapper className="mt-24 sm:mt-32 lg:mt-40">
{children}

feat

Published
Author
entr0phy4
David A.

export sections and add id's

@@ -9,23 +9,33 @@ export const article = {
},
}
+export const sections = [
+ { id: 'enumeration', title: 'Enumeration' },
+ { id: 'exploitation', title: 'Explotation' },
+ { id: 'lateral-movements', title: 'Lateral movements' },
+ { id: 'privilege-escalation', title: 'Privilege escalation' },
+]
+
export const metadata = {
title: article.title,
description: article.description,
+
}
-```json
-Victim = {
- "name": "CrossFit",
- "ip_address": "10.10.10.208",
- "difficult": "Insane",
- "os": "Linux"
+## Crossfit
+
+```js
+target = {
+ name: 'CrossFit',
+ ip_address: '10.10.10.208',
+ difficult: 'Insane',
+ os: 'Linux',
}
```
---
-### Enumeration
+### Enumeration {{ id: 'enumeration' }}
With the first [Nmap](https://nmap.org/) we try to find out the open port of the target:
@@ -177,7 +187,7 @@ ffuf reports the ftp subdomain, unfortunately, adding `ftp.crossfit.htb` to `/et
---
-### Exploitation
+### Exploitation {{ id: 'exploitation' }}
So taking advantage of the fact that we can inject code through the `User-Agent` of the request, let's try to find out what's there.
@@ -207,7 +217,7 @@ Is our code, if we decode it we get perfectly readable HTML.
echo ...bPCFET0NUWVBFIGh0bWw... | base64 -d
```
-![](/src/app/blog/CrossFit/gym-club-crossfit-htb.png)
+![](/src/app/blog/CrossFit/ftp_crossfit_htb.png)
Obviosuly, we cannot create an account by simple clicking on the "Create New Account" button.
But we can notice when hovering that, when clicking, it will request a file from `/accounts/create`, so let's try to get that HTML code the same way we did before but changing the target.
@@ -342,7 +352,7 @@ drwxr-xr-x 9 0 0 4096 May 12 2020 gym-club
drwxr-xr-x 2 0 0 4096 May 01 2020 html
```
-The forlders we found remind me of the subdomains we have been checking, so something makes me think that `development-test` is another one, but if we add it to `/etc/hosts` we still don't see different content.
+The folders we found remind me of the subdomains we have been checking, so something makes me think that `development-test` is another one, but if we add it to `/etc/hosts` we still don't see different content.
Now, we have write capability in `development-test`, so our idea will be to try to upload a reverse shell and then try to request it through the vulnerable `User-Agent` field.
@@ -388,7 +398,7 @@ The next step would be to make a proper [treatment of the teletypewriter (TTY)](
---
-### Lateral movements
+### Lateral movements {{ id: 'lateral-movements' }}
Being `www-data` user we don't really have many options, thinking about which other user to become we find our options:
@@ -658,7 +668,7 @@ Now the good stuff starts.
---
-### Privilege escalation
+### Privilege escalation {{ id: 'privilege-escalation' }}
As a first instance, being isaac we are part of the `staff` group.

feat

Published
Author
entr0phy4
David A.

add function to load sections metadata of mdx

@@ -23,6 +23,24 @@ async function loadEntries<T extends { date: string }>(
).sort((a, b) => b.date.localeCompare(a.date))
}
+export interface Section {
+ id: string
+ title: string
+}
+
+export async function loadSections(post: string) {
+ const arrPromises = await Promise.all(
+ (await glob('**/page.mdx', { cwd: `src/app/blog/${post}` })).map(
+ async (filename) => {
+ let metadata = await import(`../app/blog/${post}/${filename}`)
+ return metadata.sections
+ },
+ ),
+ )
+
+ return arrPromises[0]
+}
+
type ImagePropsWithOptionalAlt = Omit<ImageProps, 'alt'> & { alt?: string }
export type MDXEntry<T> = T & { href: string; metadata: T }

feat

Published
Author
entr0phy4
David A.

add mdx annotations, recma and remark plugin

@@ -10,6 +10,8 @@ import { remarkRehypeWrap } from 'remark-rehype-wrap'
import remarkUnwrapImages from 'remark-unwrap-images'
import shiki from 'shiki'
import { unifiedConditional } from 'unified-conditional'
+import { recmaPlugins } from './src/mdx/recma.mjs'
+import { remarkPlugins } from './src/mdx/remark.mjs'
/** @type {import('next').NextConfig} */
const nextConfig = {
@@ -54,7 +56,7 @@ export default async function config() {
let withMDX = nextMDX({
extension: /\.mdx$/,
options: {
- recmaPlugins: [recmaImportImages],
+ recmaPlugins: [recmaImportImages, recmaPlugins],
rehypePlugins: [
[rehypeShiki, { highlighter }],
[
@@ -68,6 +70,7 @@ export default async function config() {
],
remarkPlugins: [
remarkGfm,
+ remarkPlugins,
remarkUnwrapImages,
[
unifiedConditional,

fix

Published
Author
entr0phy4
David A.

correct date of crossfit post publication

@@ -1,5 +1,5 @@
export const article = {
- date: '2025-04-30',
+ date: '2025-05-04',
title: 'Machine: CrossFit',
description:
'CrossFit is an insane difficulty Linux box featuring an Apache server that hosts the website of a fictional',

config

Published
Author
entr0phy4
David A.

disable next image optimization

@@ -15,6 +15,9 @@ import { unifiedConditional } from 'unified-conditional'
const nextConfig = {
output: 'export',
distDir: 'dist',
+ images: {
+ unoptimized: true,
+ },
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
}

fix

Published
Author
entr0phy4
David A.

change path of images

@@ -86,7 +86,7 @@ We found a potential subdomain, In case virtual hosting is being done we need to
10.10.10.208 crossfit.htb gym-club.crossfit.htb
```
-![](./gym-club-crossfit-htb.png)
+![](/src/app/blog/CrossFit/gym-club-crossfit-htb.png)
Navigating around the website we found some fields in which we could try to a [Cross-Site Scripting (XSS)](https://portswigger.net/web-security/cross-site-scripting)
@@ -116,7 +116,7 @@ name=%3Cscript+src%3D%22http%3A%2F%2F10.10.16.2%3A8080%22%3E%3C%2Fscript%3E&emai
Oops, we got an anti-hacker warning.
-![](./xss_warning.png)
+![](/src/app/blog/CrossFit/xss_warning.png)
Then, they are storing information about our request and we could think that they are rendering it in some administrative panel. So, they may be a bit interested in our `User-Agent`.
@@ -171,7 +171,7 @@ ffuf -u http://gym-club.crossfit.htb \
And ffuf reports us the `ftp` subdomain.
-![](./ffuf_report.png)
+![](/src/app/blog/CrossFit/ffuf_report.png)
ffuf reports the ftp subdomain, unfortunately, adding `ftp.crossfit.htb` to `/etc/hosts` we do not get any interesting response, which makes us think that maybe the domain exists but only internally.
@@ -207,7 +207,7 @@ Is our code, if we decode it we get perfectly readable HTML.
echo ...bPCFET0NUWVBFIGh0bWw... | base64 -d
```
-![](./ftp_crossfit_htb.png)
+![](/src/app/blog/CrossFit/gym-club-crossfit-htb.png)
Obviosuly, we cannot create an account by simple clicking on the "Create New Account" button.
But we can notice when hovering that, when clicking, it will request a file from `/accounts/create`, so let's try to get that HTML code the same way we did before but changing the target.
@@ -216,7 +216,7 @@ But we can notice when hovering that, when clicking, it will request a file from
target = 'http://ftp.crossfit.htb/accounts/create'
```
-![](./create_new_account.png)
+![](/src/app/blog/CrossFit/create_new_account.png)
Checking the code:
@@ -306,7 +306,7 @@ First, we make a request to `/accounts/create`, we parse the response and extrac
If everything went well we should see our account:
-![](./account_created.png)
+![](/src/app/blog/CrossFit/account_created.png)
Creating this account was in our interest because if we remember the `Nmap` scan, port 21 it's open.
@@ -696,7 +696,7 @@ chmod +x pspy
And we run it with the flag `-f` to print file system events
-![](./pspy_execution.png)
+![](/src/app/blog/CrossFit/pspy_execution.png)
Over time, we get a lot of information, but something interesting is this binary called `dbmsg`
@@ -728,17 +728,17 @@ So, for our surgery we need tools, let's use [ghidra](https://ghidra-sre.org/) t
Create a new project
-![](./ghydra/new_project.png)
+![](/src/app/blog/CrossFit/ghydra/new_project.png)
Import a `dbmsg` as new file
-![](./ghydra/new_file.png)
+![](/src/app/blog/CrossFit/ghydra/new_file.png)
Analyze the file with default settings, and we can start
first find the function `main` on the folder `FUNCTIONS` in the sidebar
-![](./ghydra/main_function.png)
+![](/src/app/blog/CrossFit/ghydra/main_function.png)
Let's break the code

feat

Published
Author
entr0phy4
David A.

add item to navigate to home

@@ -23,6 +23,7 @@ const navigation = [
{
title: 'Content',
links: [
+ { title: 'home', href: '/' },
{ title: 'About', href: '/about' },
// { title: 'Process', href: '/process' },
{ title: 'Blog', href: '/blog' },

style

Published
Author
entr0phy4
David A.

decrease the gradient height

@@ -66,7 +66,7 @@ export function PageLinks({
}) {
return (
<div className={clsx('relative pt-24 sm:pt-32 lg:pt-40', className)}>
- <div className="absolute inset-x-0 top-0 -z-10 h-[884px] overflow-hidden rounded-t-4xl bg-linear-to-b from-[#00ff00]">
+ <div className="absolute inset-x-0 top-0 -z-10 h-[684px] overflow-hidden rounded-t-4xl bg-linear-to-b from-[#00ff00]">
<GridPattern
className="absolute inset-0 h-full w-full [mask-image:linear-gradient(to_bottom_left,white_40%,transparent_50%)] fill-black stroke-neutral-950/5"
yOffset={-270}

style

Published
Author
entr0phy4
David A.

decrease the border radius in code blocks

@@ -110,7 +110,7 @@
:where(pre) {
display: flex;
background-color: var(--color-neutral-950);
- border-radius: var(--radius-4xl);
+ border-radius: var(--radius-xl);
overflow-x: auto;
margin-top: --spacing(10);
margin-bottom: --spacing(10);