Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2026-08-28 02:03:20 +02:00
parent 198112501a
commit 5274df5c07
77 changed files with 335 additions and 8130 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

-2
View File
@@ -22,5 +22,3 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
.claude/
+2 -36
View File
@@ -10,46 +10,12 @@
# Another Hadi's Blog
My personal portfolio & blog; built with **Astro**, **Tailwind CSS** and **DaisyUI**.
Live at [hadi.icu](https://hadi.icu/?utm_source=github&utm_medium=readme).
Started from [bloomfolio](https://github.com/lauroguedes/bloomfolio), thanks for this great project!
![screenshot](./.github/assets/banner.png)
## Overview
A fast, static personal website featuring:
- **Blog**: Articles about cybersecurity, OSINT, and technology.
- **Projects**: A showcase of my open-source work.
- **RSS Feed**: Available at `/rss.xml` for blog posts & projects.
## Local Development
1. **Clone the repo:**
```bash
git clone https://github.com/anotherhadi/blog
cd anotherhadi
```
2. **Install dependencies:**
```bash
bun install
```
3. **Start the dev server:**
```bash
bun run dev
```
My personal portfolio & blog; built with **Astro** and **Tailwind CSS**.
Live at [hadi.icu](https://hadi.icu/).
---
<div align="center">
<a href="https://github.com/anotherhadi/blog">github</a> |
<a href="https://gitlab.com/anotherhadi_mirror/blog">gitlab (mirror)</a> |
<a href="https://git.hadi.icu/anotherhadi/blog">gitea (mirror)</a>
</div
+1 -13
View File
@@ -2,23 +2,11 @@
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import svelte from '@astrojs/svelte';
// https://astro.build/config
export default defineConfig({
image: {
service: { entrypoint: 'astro/assets/services/noop' }
},
site: "https://hadi.icu",
output: 'static',
integrations: [mdx(), sitemap(), svelte()],
vite: {
plugins: [tailwindcss()]
},
prefetch: {
prefetchAll: false,
defaultStrategy: 'tap'
plugins: [tailwindcss()],
},
});
+196 -393
View File
File diff suppressed because it is too large Load Diff
-2132
View File
File diff suppressed because it is too large Load Diff
Generated
-136
View File
@@ -1,136 +0,0 @@
{
"nodes": {
"bun2nix": {
"inputs": {
"flake-parts": "flake-parts",
"import-tree": "import-tree",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1770895533,
"narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=",
"owner": "nix-community",
"repo": "bun2nix",
"rev": "c843f477b15f51151f8c6bcc886954699440a6e1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "bun2nix",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1769996383,
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"import-tree": {
"locked": {
"lastModified": 1763762820,
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
"owner": "vic",
"repo": "import-tree",
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
"type": "github"
},
"original": {
"owner": "vic",
"repo": "import-tree",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1769909678,
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "72716169fe93074c333e8d0173151350670b824c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": {
"inputs": {
"bun2nix": "bun2nix",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"bun2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1770228511,
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
-73
View File
@@ -1,73 +0,0 @@
{
description = "anotherhadi blog";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
bun2nix = {
url = "github:nix-community/bun2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
bun2nix,
}: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
bun2nixPkg = bun2nix.packages.${system}.default;
blog = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "blog";
version = "1.0.0";
src = ./.;
nativeBuildInputs = [
pkgs.bun
bun2nixPkg.hook
];
bunDeps = bun2nixPkg.fetchBunDeps {
bunNix = ./bun.nix;
};
buildPhase = ''
runHook preBuild
bun run build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/share/blog
cp -r dist/* $out/share/blog/
runHook postInstall
'';
meta = {
description = "Anotherhadi's blog";
homepage = "https://github.com/anotherhadi/blog";
license = pkgs.lib.licenses.mit;
mainProgram = "blog";
};
});
in {
packages.${system} = {
inherit blog;
default = blog;
};
devShells.${system}.default = pkgs.mkShell {
packages = [
pkgs.bun
bun2nixPkg
];
};
};
}
+2 -13
View File
@@ -1,28 +1,17 @@
{
"name": "anotherhadi",
"name": "portfolio",
"type": "module",
"version": "0.0.1",
"license": "MIT",
"author": "Hadi <[email protected]>",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"fetch-data": "bun run scripts/fetch-repos.ts"
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "5.0.4",
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "^8.1.0",
"@lucide/astro": "^0.552.0",
"@tailwindcss/vite": "^4.2.4",
"@types/bun": "^1.3.13",
"astro": "6.1.9",
"daisyui": "^5.5.19",
"node-html-parser": "^7.1.0",
"svelte": "^5.55.5",
"tailwindcss": "^4.2.4"
},
"devDependencies": {
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 KiB

-1
View File
@@ -1,4 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://hadi.icu/sitemap-index.xml
-103
View File
@@ -1,103 +0,0 @@
/// <reference types="bun-types" />
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
export interface GiteaRepo {
id: number;
name: string;
full_name: string;
description: string;
html_url: string;
updated_at: string;
topics: string[];
stars_count: number;
language: string | null;
fork: boolean;
private: boolean;
website: string;
}
export interface RepoMirrors {
github?: string;
gitlab?: string;
}
export interface GiteaRepoWithMirrors extends GiteaRepo {
mirrors: RepoMirrors;
}
const GITEA_BASE = "https://git.hadi.icu";
const GITEA_USER = "anotherhadi";
const GITHUB_USER = "anotherhadi";
const GITLAB_USER = "anotherhadi_mirror";
const SKIP_REPOS = ["anotherhadi"];
async function checkMirrors(repoName: string): Promise<RepoMirrors> {
const mirrors: RepoMirrors = {};
const [githubRes, gitlabRes] = await Promise.allSettled([
fetch(`https://github.com/${GITHUB_USER}/${repoName}`, { method: "HEAD" }),
fetch(`https://gitlab.com/${GITLAB_USER}/${repoName}`, { method: "HEAD" }),
]);
if (githubRes.status === "fulfilled" && githubRes.value.ok) {
mirrors.github = `https://github.com/${GITHUB_USER}/${repoName}`;
}
if (gitlabRes.status === "fulfilled" && gitlabRes.value.ok) {
mirrors.gitlab = `https://gitlab.com/${GITLAB_USER}/${repoName}`;
}
return mirrors;
}
export async function fetchGiteaRepos(): Promise<GiteaRepoWithMirrors[]> {
try {
const res = await fetch(
`${GITEA_BASE}/api/v1/users/${GITEA_USER}/repos?limit=50&page=1`,
);
if (!res.ok) throw new Error(`Gitea API: ${res.status}`);
const repos: GiteaRepo[] = await res.json();
const filtered = repos
.filter((r) => !r.fork && !r.private && !SKIP_REPOS.includes(r.name))
.sort(
(a, b) =>
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
);
const reposWithMirrors = await Promise.all(
filtered.map(async (repo) => ({
...repo,
mirrors: await checkMirrors(repo.name),
})),
);
return reposWithMirrors;
} catch (e) {
console.error("Failed to fetch Gitea repos:", e);
return [];
}
}
export function getBannerUrl(repo: GiteaRepo): string {
return `${GITEA_BASE}/${repo.full_name}/raw/branch/main/.github/assets/banner.png`;
}
async function main() {
console.log("Fetching repos from Gitea...");
const rawRepos = await fetchGiteaRepos();
const repos = rawRepos.map((repo) => ({
...repo,
banner_url: `${GITEA_BASE}/${repo.full_name}/raw/branch/main/.github/assets/banner.png`,
}));
const dataDir = join(process.cwd(), "src/data");
await mkdir(dataDir, { recursive: true });
await Bun.write(join(dataDir, "repos.json"), JSON.stringify(repos, null, 2));
console.log(`Saved ${repos.length} repos to src/data/repos.json`);
}
main().catch(console.error);
-26
View File
@@ -1,26 +0,0 @@
---
import { Image } from "astro:assets";
const avatar = "/avatar.jpg";
const username = "anotherhadi";
const bio = "Infosec engineer.";
---
<div class="flex flex-wrap gap-3 justify-start">
<div
class="ring-base-300 ring-offset-base-100 rounded-full ring-2 ring-offset-2 flex justify-center items-center"
>
<Image
src={avatar}
alt="anotherhadi avatar"
class="rounded-full m-auto"
width={36}
height={36}
/>
</div>
<div>
<p class="text-sm font-semibold">
<a href="/"><span class="text-base-content/40">@</span>{username}</a>
</p>
<p class="text-xs text-base-content/60">{bio}</p>
</div>
</div>
-36
View File
@@ -1,36 +0,0 @@
---
import { ArrowUp } from "@lucide/astro";
---
<button
id="back-to-top"
class="btn btn-circle btn-primary fixed bottom-6 right-6 z-50 transition-opacity duration-300 opacity-0 invisible"
aria-label="Back to top"
>
<ArrowUp />
</button>
<script>
const backToTopBtn = document.getElementById("back-to-top");
if (backToTopBtn) {
// Afficher/Masquer le bouton selon le scroll
window.addEventListener("scroll", () => {
if (window.scrollY > 300) {
backToTopBtn.classList.remove("opacity-0", "invisible");
backToTopBtn.classList.add("opacity-100", "visible");
} else {
backToTopBtn.classList.add("opacity-0", "invisible");
backToTopBtn.classList.remove("opacity-100", "visible");
}
});
// Action de retour en haut
backToTopBtn.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
}
</script>
-43
View File
@@ -1,43 +0,0 @@
---
import { getCollection } from "astro:content";
import BlogCard from "./BlogCard.astro";
import { ArrowRight } from "@lucide/astro";
const blogEntries = await getCollection("blog");
// Sort by publish date, most recent first
const sortedPosts = blogEntries.sort(
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
);
// Get only the latest 3 posts
const latestPosts = sortedPosts.slice(0, 3);
---
<section id="blog" class="py-20 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Latest Blog Posts</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{
latestPosts.map((post) => (
<BlogCard
displayBanner={false}
displayTags={false}
displayDate={false}
post={post}
/>
))
}
</div>
<div class="text-center mt-12">
<a href="/blog" class="btn btn-ghost gap-2">
View All Posts
<ArrowRight class="size-4" />
</a>
</div>
</div>
</section>
-56
View File
@@ -1,56 +0,0 @@
---
import type { CollectionEntry } from "astro:content";
import TagBadge from "./TagBadge.astro";
import { Image } from "astro:assets";
import { formatDate } from "../utils/notes";
interface Props {
displayBanner: boolean;
displayDate: boolean;
displayTags: boolean;
post: CollectionEntry<"blog">;
}
const { post } = Astro.props;
---
<article
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
>
{ Astro.props.displayBanner && (
<figure class="aspect-video">
<Image
src={post.data.image}
alt={post.data.title}
class="w-full h-full object-cover"
width={600}
height={400}
/>
</figure>
)}
<div class="card-body">
{ Astro.props.displayDate && (
<time class="text-sm text-base-content/60">
{formatDate(post.data.publishDate)}
</time>
)}
<h2 class="card-title hover:text-primary transition-colors">
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</h2>
<p class="text-base-content/80">{post.data.description}</p>
{ Astro.props.displayTags && post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-2">
{post.data.tags.map((tag) => (
<TagBadge tag={tag} />
))}
</div>
)}
<div class="card-actions justify-end mt-4">
<a href={`/blog/${post.id}`} class="btn btn-soft btn-primary btn-sm">
Read More
</a>
</div>
</div>
</article>
-12
View File
@@ -1,12 +0,0 @@
---
---
<script>
function message() {
console.log(
"Hey!\nWant to chat? Send me a mail to anotherhadi.clapped234[at]passmail.net and I'll respond whenever I can.\nUse my gpg key (https://hadi.icu/anotherhadi.asc) for secure communication.",
);
}
message();
</script>
-20
View File
@@ -1,20 +0,0 @@
---
---
<section id="contact" class="py-20 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4 flex gap-5 justify-center m-auto">
Want to chat?
</h2>
<p class="text-lg text-base-content/70 max-w-2xl m-auto">
Send me a mail to <span class="text-primary"
>anotherhadi.clapped234[at]passmail.net</span
> and I'll respond whenever I can.
<br />Use my <a class="text-primary" href="/anotherhadi.asc">gpg key</a>
for secure communication.
</p>
</div>
</div>
</section>
-122
View File
@@ -1,122 +0,0 @@
---
import { ExternalLink, ChevronDown } from "@lucide/astro";
interface Props {
displayBanner: boolean;
repo: {
name: string;
description: string;
html_url: string;
website: string;
topics: string[];
banner_url: string;
mirrors: {
github?: string;
gitlab?: string;
};
};
}
const { repo } = Astro.props;
const platforms = [
...(repo.mirrors.github
? [{ label: "GitHub", url: repo.mirrors.github }]
: []),
...(repo.mirrors.gitlab
? [{ label: "GitLab", url: repo.mirrors.gitlab }]
: []),
{ label: "Gitea", url: repo.html_url },
];
const hasMultiplePlatforms = platforms.length > 1;
---
<article
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
>
{ Astro.props.displayBanner && repo.banner_url && (
<figure class="aspect-video bg-base-200 overflow-hidden">
<img
src={repo.banner_url}
alt={repo.name}
class="w-full h-full object-cover"
onerror="this.parentElement.style.display='none'"
/>
</figure>
)}
<div class="card-body">
<h2 class="card-title hover:text-primary transition-colors">
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
{repo.name}
</a>
</h2>
{repo.description && <p class="text-base-content/80">{repo.description}</p>}
<div class="flex flex-wrap gap-2 mt-2">
{
repo.topics.map((topic) => (
<span class="badge badge-sm rounded-sm badge-soft badge-accent">
{topic}
</span>
))
}
</div>
<div class="card-actions justify-end mt-4 gap-2">
{
repo.website && (
<a
href={repo.website}
target="_blank"
rel="noopener noreferrer"
class="btn btn-soft btn-sm gap-1"
>
<ExternalLink class="size-4" />
Website
</a>
)
}
{
hasMultiplePlatforms ? (
<div class="dropdown dropdown-end">
<div
tabindex="0"
role="button"
class="btn btn-soft btn-primary btn-sm gap-1"
>
<ExternalLink class="size-4" />
View Source
<ChevronDown class="size-3" />
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-xl z-10 w-36 p-1.5 shadow border border-base-200 text-sm"
>
{platforms.map(({ label, url }) => (
<li>
<a href={url} target="_blank" rel="noopener noreferrer">
{label}
</a>
</li>
))}
</ul>
</div>
) : (
<a
href={repo.html_url}
target="_blank"
rel="noopener noreferrer"
class="btn btn-soft btn-primary btn-sm gap-1"
>
<ExternalLink class="size-4" />
View on Gitea
</a>
)
}
</div>
</div>
</article>
-337
View File
@@ -1,337 +0,0 @@
---
import { FolderCode, Key, Rss } from "@lucide/astro";
import { Image } from "astro:assets";
import type { SocialLinks } from "../config";
interface Props {
name: string;
title: string;
description: string;
avatar: any;
socialLinks?: SocialLinks;
gpgKey?: string;
rssFeed?: string;
}
const {
name,
title,
description,
avatar,
socialLinks,
gpgKey,
rssFeed,
} = Astro.props;
---
<section class="hero py-20">
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-10">
<div class="avatar">
<div
class="w-32 md:w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
>
<Image src={avatar} alt={name} />
</div>
</div>
<div class="max-w-2xl">
<h1 class="text-5xl font-bold mb-4">
Hi, I'm {name}
</h1>
<p class="text-xl text-base-content/80 mb-2">{title}</p>
<p class="text-lg max-w-lg leading-relaxed mb-6">
{description}
</p>
{
socialLinks && (
<div class="flex flex-wrap gap-4">
{socialLinks.github && (
<div class="tooltip" data-tip="Github">
<a
href={socialLinks.github}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle btn-ghost"
aria-label="GitHub"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
</div>
)}
{socialLinks.gitlab && (
<div class="tooltip" data-tip="Gitlab">
<a
href={socialLinks.gitlab}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle btn-ghost"
aria-label="Gitlab"
>
<svg
width="24"
height="24"
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z" />
</svg>
</a>
</div>
)}
{socialLinks.gitea && (
<div class="tooltip" data-tip="Gitea">
<a
href={socialLinks.gitea}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle btn-ghost"
aria-label="Gitea"
>
<svg
width="24"
height="24"
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z" />
</svg>
</a>
</div>
)}
{socialLinks.linkedin && (
<div class="tooltip" data-tip="Linkedin">
<a
href={socialLinks.linkedin}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle btn-ghost"
aria-label="LinkedIn"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
</a>
</div>
)}
{socialLinks.twitter && (
<div class="tooltip" data-tip="Twitter/X">
<a
href={socialLinks.twitter}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle btn-ghost"
aria-label="X (Twitter)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 32 32"
fill="currentColor"
>
<path d="M18.42,14.009L27.891,3h-2.244l-8.224,9.559L10.855,3H3.28l9.932,14.455L3.28,29h2.244l8.684-10.095,6.936,10.095h7.576l-10.301-14.991h0Zm-3.074,3.573l-1.006-1.439L6.333,4.69h3.447l6.462,9.243,1.006,1.439,8.4,12.015h-3.447l-6.854-9.804h0Z" />
</svg>
</a>
</div>
)}
{socialLinks.bluesky && (
<div class="tooltip" data-tip="Bluesky">
<a
href={socialLinks.bluesky}
class="btn btn-circle btn-ghost"
aria-label="Bluesky"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 32 32"
fill="currentColor"
>
<path d="M23.931,5.298c-3.21,2.418-6.663,7.32-7.931,9.951-1.267-2.631-4.721-7.533-7.931-9.951-2.316-1.744-6.069-3.094-6.069,1.201,0,.857,.49,7.206,.778,8.237,.999,3.583,4.641,4.497,7.881,3.944-5.663,.967-7.103,4.169-3.992,7.372,5.908,6.083,8.492-1.526,9.154-3.476,.123-.36,.179-.527,.179-.379,0-.148,.057,.019,.179,.379,.662,1.949,3.245,9.558,9.154,3.476,3.111-3.203,1.671-6.405-3.992-7.372,3.24,.553,6.882-.361,7.881-3.944,.288-1.031,.778-7.38,.778-8.237,0-4.295-3.753-2.945-6.069-1.201Z" />
</svg>
</a>
</div>
)}
{socialLinks.instagram && (
<div class="tooltip" data-tip="Instagram">
<a
href={`${socialLinks.instagram}`}
class="btn btn-circle btn-ghost"
aria-label="Instagram"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 32 32"
>
<path d="M10.202,2.098c-1.49,.07-2.507,.308-3.396,.657-.92,.359-1.7,.84-2.477,1.619-.776,.779-1.254,1.56-1.61,2.481-.345,.891-.578,1.909-.644,3.4-.066,1.49-.08,1.97-.073,5.771s.024,4.278,.096,5.772c.071,1.489,.308,2.506,.657,3.396,.359,.92,.84,1.7,1.619,2.477,.779,.776,1.559,1.253,2.483,1.61,.89,.344,1.909,.579,3.399,.644,1.49,.065,1.97,.08,5.771,.073,3.801-.007,4.279-.024,5.773-.095s2.505-.309,3.395-.657c.92-.36,1.701-.84,2.477-1.62s1.254-1.561,1.609-2.483c.345-.89,.579-1.909,.644-3.398,.065-1.494,.081-1.971,.073-5.773s-.024-4.278-.095-5.771-.308-2.507-.657-3.397c-.36-.92-.84-1.7-1.619-2.477s-1.561-1.254-2.483-1.609c-.891-.345-1.909-.58-3.399-.644s-1.97-.081-5.772-.074-4.278,.024-5.771,.096m.164,25.309c-1.365-.059-2.106-.286-2.6-.476-.654-.252-1.12-.557-1.612-1.044s-.795-.955-1.05-1.608c-.192-.494-.423-1.234-.487-2.599-.069-1.475-.084-1.918-.092-5.656s.006-4.18,.071-5.656c.058-1.364,.286-2.106,.476-2.6,.252-.655,.556-1.12,1.044-1.612s.955-.795,1.608-1.05c.493-.193,1.234-.422,2.598-.487,1.476-.07,1.919-.084,5.656-.092,3.737-.008,4.181,.006,5.658,.071,1.364,.059,2.106,.285,2.599,.476,.654,.252,1.12,.555,1.612,1.044s.795,.954,1.051,1.609c.193,.492,.422,1.232,.486,2.597,.07,1.476,.086,1.919,.093,5.656,.007,3.737-.006,4.181-.071,5.656-.06,1.365-.286,2.106-.476,2.601-.252,.654-.556,1.12-1.045,1.612s-.955,.795-1.608,1.05c-.493,.192-1.234,.422-2.597,.487-1.476,.069-1.919,.084-5.657,.092s-4.18-.007-5.656-.071M21.779,8.517c.002,.928,.755,1.679,1.683,1.677s1.679-.755,1.677-1.683c-.002-.928-.755-1.679-1.683-1.677,0,0,0,0,0,0-.928,.002-1.678,.755-1.677,1.683m-12.967,7.496c.008,3.97,3.232,7.182,7.202,7.174s7.183-3.232,7.176-7.202c-.008-3.97-3.233-7.183-7.203-7.175s-7.182,3.233-7.174,7.203m2.522-.005c-.005-2.577,2.08-4.671,4.658-4.676,2.577-.005,4.671,2.08,4.676,4.658,.005,2.577-2.08,4.671-4.658,4.676-2.577,.005-4.671-2.079-4.676-4.656h0" />
</svg>
</a>
</div>
)}
{socialLinks.youtube && (
<div class="tooltip" data-tip="Youtube">
<a
href={socialLinks.youtube}
class="btn btn-circle btn-ghost"
aria-label="YouTube"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 32 32"
fill="currentColor"
>
<path d="M31.331,8.248c-.368-1.386-1.452-2.477-2.829-2.848-2.496-.673-12.502-.673-12.502-.673,0,0-10.007,0-12.502,.673-1.377,.37-2.461,1.462-2.829,2.848-.669,2.512-.669,7.752-.669,7.752,0,0,0,5.241,.669,7.752,.368,1.386,1.452,2.477,2.829,2.847,2.496,.673,12.502,.673,12.502,.673,0,0,10.007,0,12.502-.673,1.377-.37,2.461-1.462,2.829-2.847,.669-2.512,.669-7.752,.669-7.752,0,0,0-5.24-.669-7.752ZM12.727,20.758V11.242l8.364,4.758-8.364,4.758Z" />
</svg>
</a>
</div>
)}
{socialLinks.medium && (
<div class="tooltip" data-tip="Medium">
<a
href={socialLinks.medium}
class="btn btn-circle btn-ghost"
aria-label="Medium"
target="_blank"
>
<svg
class="size-6"
viewBox="0 0 233 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_244_2)">
<path
d="M174.104 9.73402L174.319 9.68702V8.02002H130.39L89.6001 103.89L48.8101 8.02102H1.47205V9.68702L1.68405 9.73402C9.70205 11.544 13.7741 14.243 13.7741 23.976V137.93C13.7741 147.664 9.68705 150.363 1.66805 152.173L1.45605 152.22V153.891H33.5741V152.226L33.3611 152.178C25.3431 150.369 21.2721 147.669 21.2721 137.936V30.586L73.6711 153.891H76.6431L130.568 27.148V140.75C129.881 148.438 125.847 150.812 118.586 152.451L118.371 152.501V154.153H174.319V152.501L174.104 152.451C166.835 150.812 162.704 148.438 162.017 140.75L161.98 23.976H162.017C162.017 14.243 166.088 11.544 174.104 9.73402ZM199.659 85.222C200.574 64.748 207.927 49.97 220.265 49.715C224.071 49.778 227.263 51.027 229.744 53.429C235.016 58.547 237.495 69.241 237.112 85.222H199.659ZM199.106 90.992H264.679V90.717C264.493 75.061 259.958 62.883 251.213 54.521C243.654 47.294 232.462 43.318 220.706 43.318H220.443C214.342 43.318 206.859 44.798 201.534 47.478C195.473 50.285 190.127 54.481 185.679 59.989C178.518 68.863 174.18 80.855 173.125 94.332C173.092 94.736 173.061 95.14 173.033 95.544C173.007 95.9274 172.985 96.3111 172.968 96.695C172.874 98.5898 172.842 100.487 172.874 102.384C173.584 132.908 190.072 157.301 219.357 157.301C245.062 157.301 260.032 138.51 263.764 113.288L261.878 112.624C255.321 126.18 243.544 134.395 230.14 133.393C211.843 132.024 197.826 113.471 199.098 90.998"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_244_2">
<rect width="233" height="160" fill="white" />
</clipPath>
</defs>
</svg>
</a>
</div>
)}
{gpgKey && (
<div class="tooltip" data-tip="Gpg key">
<a
href={gpgKey}
class="btn btn-circle btn-ghost"
aria-label="Gpg Key"
target="_blank"
>
<Key class="size-6" />
</a>
</div>
)}
{socialLinks.kofi && (
<div class="tooltip" data-tip="Ko-fi - Support me">
<a
href={socialLinks.kofi}
class="btn btn-circle btn-ghost"
aria-label="Ko-fi"
target="_blank"
>
<svg
class="size-6"
viewBox="0 0 241 194"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_245_13)">
<mask
id="mask0_245_13"
style="mask-type:luminance"
maskUnits="userSpaceOnUse"
x="-1"
y="0"
width="242"
height="194"
>
<path
d="M240.469 0.958984H-0.00585938V193.918H240.469V0.958984Z"
fill="white"
/>
</mask>
<g mask="url(#mask0_245_13)">
<path
d="M114.544 16.1589C151.372 16.1589 160.92 16.6152 174.56 17.9773C206.617 21.1578 225.255 40.9376 225.255 69.3582V72.9939C225.255 99.3685 205.932 120.966 179.786 123.234C177.74 130.058 174.559 136.874 170.238 143.698C160.235 159.156 140.228 178.707 103.4 178.707H96.127C66.1161 178.707 42.9278 165.751 29.0596 142.107C16.7815 121.422 15.1914 98.4562 15.1914 67.7673L15.2109 66.3601C15.7669 37.0206 33.7206 21.1214 54.7559 18.4333C70.8986 16.387 90.9061 16.1589 114.544 16.1589ZM114.537 33.2087C89.7595 33.2087 72.0262 33.4368 58.6143 35.4832C39.7477 38.2139 32.2402 48.8954 32.2402 67.99L32.252 70.7087C32.3676 98.4946 34.3633 116.731 43.6123 133.689C54.5221 153.924 74.3017 161.653 96.8115 161.653H103.856C133.41 161.653 147.735 147.329 155.692 134.829C159.557 128.462 162.965 121.417 164.783 112.547L166.146 106.864H174.332C192.521 106.864 208.208 92.0898 208.208 73.2166V69.8083C208.208 48.6671 195.024 37.5227 172.058 34.7986C159.102 33.6645 151.371 33.2087 114.537 33.2087Z"
fill="white"
/>
<path
d="M166.158 83.6801C166.158 86.4107 168.204 88.4572 171.841 88.4572C183.435 88.4572 189.802 81.8619 189.802 70.9523C189.802 60.0427 183.435 53.2195 171.841 53.2195C168.204 53.2195 166.158 55.2657 166.158 57.9963V83.6866V83.6801Z"
fill="white"
/>
<path
d="M54.5319 82.3198C54.5319 95.732 62.033 107.326 71.5805 116.424C77.9476 122.562 87.9513 128.93 94.7683 133.022C96.8145 134.157 98.8609 134.841 101.136 134.841C103.866 134.841 106.134 134.157 107.959 133.022C114.782 128.93 124.779 122.562 130.919 116.424C140.694 107.332 148.195 95.7383 148.195 82.3198C148.195 67.7673 137.286 54.8115 121.599 54.8115C112.28 54.8115 105.912 59.5882 101.136 66.1772C96.8145 59.582 90.2257 54.8115 80.8999 54.8115C64.9853 54.8115 54.5254 67.7673 54.5254 82.3198"
fill="#FF5A16"
/>
</g>
</g>
<defs>
<clipPath id="clip0_245_13">
<rect width="241" height="194" fill="white" />
</clipPath>
</defs>
</svg>
</a>
</div>
)}
{socialLinks.codetips && (
<div class="tooltip" data-tip="Codetips">
<a
href={socialLinks.codetips}
class="btn btn-circle btn-ghost"
aria-label="CodeTips"
target="_blank"
>
<FolderCode class="size-6" />
</a>
</div>
)}
{rssFeed && (
<div class="tooltip" data-tip="RSS Feed">
<a
href={rssFeed}
class="btn btn-circle btn-ghost"
aria-label="RSS Feed"
target="_blank"
>
<Rss class="size-6" />
</a>
</div>
)}
</div>
)
}
</div>
</div>
</section>
-142
View File
@@ -1,142 +0,0 @@
---
const pathname = Astro.url.pathname;
const links = [
{ href: "/", label: "home" },
{ href: "/blog", label: "blog" },
{ href: "/notes", label: "infosec notes" },
{ href: "/projects", label: "projects" },
];
function isActive(href: string) {
if (href === "/") return pathname === "/";
return pathname.startsWith(href);
}
---
<header
class="fixed top-0 left-0 right-0 z-[60] h-12 flex items-center px-5"
style="background: oklch(0% 0 0 / 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid oklch(22% 0 0);"
>
<div
class="flex items-center justify-between w-full max-w-screen-2xl mx-auto"
>
<a
href="/"
class="font-mono text-sm text-base-content/40 hover:text-primary transition-colors duration-200 tracking-tight"
>
~/hadi
</a>
<div
id="oneko-track"
transition:persist
class="flex-1 relative h-12 pointer-events-none"
>
</div>
<nav class="hidden md:flex items-center">
{
links.map((link) => (
<a
href={link.href}
class:list={[
"font-mono text-xs px-3 py-1.5 transition-colors duration-150",
isActive(link.href)
? "text-primary"
: "text-base-content/40 hover:text-base-content/80",
]}
>
{isActive(link.href) ? (
<span class="flex items-center gap-1">
<span class="inline-block w-1 h-1 rounded-full bg-primary" />
{link.label}
</span>
) : (
link.label
)}
</a>
))
}
</nav>
<button
id="hamburger"
class="md:hidden flex flex-col gap-1 p-2 text-base-content/40 hover:text-base-content/70 transition-colors"
aria-label="Toggle menu"
>
<span
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
></span>
<span
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
></span>
<span
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
></span>
</button>
</div>
</header>
<div
id="mobile-menu"
class="hidden fixed inset-x-0 top-12 z-[59] md:hidden border-b border-base-300/60 py-2"
style="background: oklch(2% 0 0 / 0.97); backdrop-filter: blur(12px);"
>
{
links.map((link) => (
<a
href={link.href}
class:list={[
"flex items-center gap-2 font-mono text-sm px-5 py-2.5 transition-colors",
isActive(link.href)
? "text-primary"
: "text-base-content/45 hover:text-base-content/80",
]}
>
{isActive(link.href) && (
<span class="inline-block w-1 h-1 rounded-full bg-primary shrink-0" />
)}
{link.label}
</a>
))
}
</div>
<script>
function init() {
const btn = document.getElementById("hamburger");
const menu = document.getElementById("mobile-menu");
if (!btn || !menu) return;
const lines = btn.querySelectorAll<HTMLElement>(".hamburger-line");
let open = false;
menu.style.display = "none";
lines[0].style.transform = "";
lines[1].style.opacity = "";
lines[2].style.transform = "";
btn.addEventListener("click", () => {
open = !open;
menu.style.display = open ? "block" : "none";
lines[0].style.transform = open ? "translateY(5px) rotate(45deg)" : "";
lines[1].style.opacity = open ? "0" : "";
lines[2].style.transform = open ? "translateY(-5px) rotate(-45deg)" : "";
});
document.addEventListener("click", (e) => {
if (
open &&
!btn.contains(e.target as Node) &&
!menu.contains(e.target as Node)
) {
open = false;
menu.style.display = "none";
lines[0].style.transform = "";
lines[1].style.opacity = "";
lines[2].style.transform = "";
}
});
}
document.addEventListener("astro:page-load", init);
</script>
-279
View File
@@ -1,279 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
interface GNode {
id: string;
title: string;
current: boolean;
}
interface GEdge {
from: string;
to: string;
}
interface Props {
nodes?: GNode[];
edges?: GEdge[];
}
const { nodes = [], edges = [] }: Props = $props();
const PRIMARY = "oklch(71% 0.0863 296.59)";
let canvas: HTMLCanvasElement;
function startAnimation(): () => void {
if (!canvas || nodes.length === 0) return () => {};
const W = (canvas.width = canvas.offsetWidth);
const H = (canvas.height = 190);
const ctx = canvas.getContext("2d")!;
type SimNode = GNode & { x: number; y: number; vx: number; vy: number };
const simNodes: SimNode[] = nodes.map((n) => ({
...n,
x: n.current ? W / 2 : W / 2 + (Math.random() - 0.5) * 80,
y: n.current ? H / 2 : H / 2 + (Math.random() - 0.5) * 80,
vx: 0,
vy: 0,
}));
let dragging: SimNode | null = null;
let hovered: SimNode | null = null;
function nodeAt(x: number, y: number): SimNode | null {
return (
simNodes.find((n) => {
const dx = n.x - x,
dy = n.y - y;
return Math.sqrt(dx * dx + dy * dy) < (n.current ? 10 : 8);
}) ?? null
);
}
function tick() {
for (let i = 0; i < simNodes.length; i++) {
for (let j = i + 1; j < simNodes.length; j++) {
const a = simNodes[i],
b = simNodes[j];
const dx = b.x - a.x,
dy = b.y - a.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const f = 900 / (d * d);
a.vx -= (dx / d) * f;
a.vy -= (dy / d) * f;
b.vx += (dx / d) * f;
b.vy += (dy / d) * f;
}
}
for (const e of edges) {
const a = simNodes.find((n) => n.id === e.from);
const b = simNodes.find((n) => n.id === e.to);
if (!a || !b) continue;
const dx = b.x - a.x,
dy = b.y - a.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const f = (d - 75) * 0.04;
a.vx += (dx / d) * f;
a.vy += (dy / d) * f;
b.vx -= (dx / d) * f;
b.vy -= (dy / d) * f;
}
for (const n of simNodes) {
n.vx += (W / 2 - n.x) * 0.025;
n.vy += (H / 2 - n.y) * 0.025;
}
for (const n of simNodes) {
if (n === dragging) continue;
n.vx *= 0.78;
n.vy *= 0.78;
n.x = Math.max(16, Math.min(W - 16, n.x + n.vx));
n.y = Math.max(16, Math.min(H - 16, n.y + n.vy));
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = "oklch(2% 0 0)";
ctx.fillRect(0, 0, W, H);
const connected = new Set<string>();
if (hovered) {
for (const e of edges) {
if (e.from === hovered.id) connected.add(e.to);
if (e.to === hovered.id) connected.add(e.from);
}
}
for (const e of edges) {
const a = simNodes.find((n) => n.id === e.from);
const b = simNodes.find((n) => n.id === e.to);
if (!a || !b) continue;
const lit =
hovered && (e.from === hovered.id || e.to === hovered.id);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = lit ? "oklch(55% 0 0)" : "oklch(27% 0 0)";
ctx.lineWidth = lit ? 1.5 : 1;
ctx.stroke();
}
for (const n of simNodes) {
const isHov = hovered?.id === n.id;
const isCon = connected.has(n.id);
const r = n.current ? 7 : isHov ? 6 : 4.5;
if (isHov && !n.current) {
ctx.beginPath();
ctx.arc(n.x, n.y, r + 5, 0, Math.PI * 2);
ctx.fillStyle = "oklch(71% 0.0863 296.59 / 0.15)";
ctx.fill();
}
ctx.beginPath();
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
ctx.fillStyle = n.current
? PRIMARY
: isHov
? "oklch(78% 0.05 296.59)"
: isCon
? "oklch(58% 0.03 296.59)"
: "oklch(40% 0 0)";
ctx.fill();
if (n.current || isHov || isCon) {
ctx.font = `${n.current ? "10px" : "9px"} monospace`;
ctx.textAlign = "center";
ctx.fillStyle = n.current ? "oklch(87% 0 0)" : "oklch(62% 0 0)";
const label =
n.title.length > 14 ? n.title.slice(0, 13) + "…" : n.title;
ctx.fillText(label, n.x, n.y + r + 9);
}
}
}
let animId: number;
function loop() {
tick();
draw();
animId = requestAnimationFrame(loop);
}
animId = requestAnimationFrame(loop);
const onMousedown = (e: MouseEvent) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
dragging = nodeAt(
(e.clientX - r.left) * sx,
(e.clientY - r.top) * (H / canvas.offsetHeight),
);
};
const onMousemove = (e: MouseEvent) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
const x = (e.clientX - r.left) * sx;
const y = (e.clientY - r.top) * (H / canvas.offsetHeight);
if (dragging) {
dragging.x = x;
dragging.y = y;
dragging.vx = 0;
dragging.vy = 0;
}
hovered = nodeAt(x, y);
canvas.style.cursor =
hovered && !hovered.current ? "pointer" : "default";
};
const onMouseup = () => {
dragging = null;
};
const onMouseleave = () => {
dragging = null;
hovered = null;
};
const onClick = (e: MouseEvent) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
const n = nodeAt(
(e.clientX - r.left) * sx,
(e.clientY - r.top) * (H / canvas.offsetHeight),
);
if (n && !n.current) window.location.href = `/notes/${n.id}`;
};
canvas.addEventListener("mousedown", onMousedown);
canvas.addEventListener("mousemove", onMousemove);
canvas.addEventListener("mouseup", onMouseup);
canvas.addEventListener("mouseleave", onMouseleave);
canvas.addEventListener("click", onClick);
return () => {
cancelAnimationFrame(animId);
canvas.removeEventListener("mousedown", onMousedown);
canvas.removeEventListener("mousemove", onMousemove);
canvas.removeEventListener("mouseup", onMouseup);
canvas.removeEventListener("mouseleave", onMouseleave);
canvas.removeEventListener("click", onClick);
};
}
onMount(() => {
const drawer = document.getElementById(
"graph-drawer",
) as HTMLInputElement | null;
const outerDrawer =
drawer?.closest<HTMLElement>(".drawer.drawer-end") ?? null;
let stopFn: (() => void) | null = null;
function isVisible() {
return (
(drawer?.checked ?? false) ||
(outerDrawer?.classList.contains("xl:drawer-open") ?? false)
);
}
function start() {
if (stopFn) return;
stopFn = startAnimation();
}
function stop() {
stopFn?.();
stopFn = null;
}
if (isVisible()) start();
const onDrawerChange = () => {
isVisible() ? start() : stop();
};
drawer?.addEventListener("change", onDrawerChange);
// Watch for xl:drawer-open class toggled by the graph button script
const observer = new MutationObserver(() => {
isVisible() ? start() : stop();
});
if (outerDrawer) {
observer.observe(outerDrawer, {
attributes: true,
attributeFilter: ["class"],
});
}
return () => {
stop();
drawer?.removeEventListener("change", onDrawerChange);
observer.disconnect();
};
});
</script>
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
<canvas
bind:this={canvas}
height="190"
role="img"
aria-label="Graph of linked notes"
style="width:100%; display:block; background: oklch(2% 0 0); cursor:default;"
></canvas>
-130
View File
@@ -1,130 +0,0 @@
---
import NoteGraph from "./NoteGraph.svelte";
import Author from "./Author.astro";
import { formatDate } from "../utils/notes";
import type { CollectionEntry } from "astro:content";
interface Props {
entry: CollectionEntry<"notes">;
graphNodes: { id: string; title: string; current: boolean }[];
graphEdges: { from: string; to: string }[];
forwardLinks: CollectionEntry<"notes">[];
backlinks: CollectionEntry<"notes">[];
externalLinks: { url: string; label: string }[];
}
const { entry, graphNodes, graphEdges, forwardLinks, backlinks, externalLinks } = Astro.props;
---
<aside
id="right-sidebar"
class="w-52 flex flex-col border-l border-base-300/60 h-full overflow-y-auto"
style="background: oklch(4% 0 0);"
>
<div class="border-b border-base-300/40">
<p
class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest px-3 pt-3 pb-2"
>
graph
</p>
<NoteGraph client:load nodes={graphNodes} edges={graphEdges} />
{
graphNodes.length < 2 && (
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
no connections yet
</p>
)
}
</div>
{
forwardLinks.length > 0 && (
<div class="p-3 border-b border-base-300/40">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
links
</p>
<ul class="space-y-1">
{forwardLinks.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
>
→ {n.data.title}
</a>
</li>
))}
</ul>
</div>
)
}
{
backlinks.length > 0 && (
<div class="p-3">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
backlinks
</p>
<ul class="space-y-1">
{backlinks.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
>
← {n.data.title}
</a>
</li>
))}
</ul>
</div>
)
}
{
forwardLinks.length === 0 && backlinks.length === 0 && (
<div class="p-3">
<p class="font-mono text-[9px] text-base-content/20">
no linked notes
</p>
</div>
)
}
{
externalLinks.length > 0 && (
<div class="p-3 border-t border-base-300/40">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
external
</p>
<ul class="space-y-1">
{externalLinks.map(({ url, label }) => (
<li>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
title={url}
>
↗ {label}
</a>
</li>
))}
</ul>
</div>
)
}
<div class="px-4 pt-4 pb-1 border-t border-base-300/40 mt-auto">
<time
datetime={entry.data.publishDate.toISOString()}
class="font-mono text-[10px] text-base-content/30 uppercase tracking-widest"
>
{formatDate(entry.data.publishDate)}
</time>
</div>
<div class="px-4 py-4">
<Author />
</div>
</aside>
-152
View File
@@ -1,152 +0,0 @@
<script lang="ts">
import { slide } from "svelte/transition";
import { untrack } from "svelte";
interface Note {
id: string;
data: { title: string; tags: string[]; category?: string };
body?: string;
}
interface Props {
notes: Note[];
currentEntry?: Note;
currentCategory?: string;
categories: string[];
}
const { notes, currentEntry, currentCategory, categories }: Props = $props();
let search = $state("");
function getCategory(n: Note): string {
if (n.data.category) return n.data.category;
const parts = n.id.split("/");
return parts.length > 1 ? parts[0] : "General";
}
function extractInlineHashtags(body: string): string[] {
const re = /#(\w+)/g;
const tags: string[] = [];
let m;
while ((m = re.exec(body)) !== null) tags.push(m[1].toLowerCase());
return [...new Set(tags)];
}
function matchesSearch(note: Note): boolean {
const raw = search.toLowerCase().trim();
if (!raw) return true;
const isTag = raw.startsWith("#");
const term = isTag ? raw.slice(1) : raw;
const title = note.data.title.toLowerCase();
const tags = [
...note.data.tags,
...extractInlineHashtags(note.body ?? ""),
].map((t) => t.toLowerCase());
return isTag
? tags.some((t) => t.includes(term))
: title.includes(term) || tags.join(",").includes(term);
}
const activeCategory = $derived(
currentCategory ?? (currentEntry ? getCategory(currentEntry) : null),
);
let openCategories = $state<string[]>(
untrack(() => categories.filter((c) => c === activeCategory)),
);
function toggle(cat: string) {
if (openCategories.includes(cat)) {
openCategories = openCategories.filter((c) => c !== cat);
} else {
openCategories = [...openCategories, cat];
}
}
</script>
<div class="flex flex-col flex-1 min-h-0">
<div class="px-3 py-3 border-b border-base-300/40 shrink-0">
<div
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-base-200/50 border border-base-300/40 focus-within:border-base-300/70 transition-colors"
>
<span class="text-base-content/30 font-mono text-xs shrink-0">›</span>
<input
type="text"
placeholder="search..."
bind:value={search}
class="flex-1 min-w-0 bg-transparent text-xs font-mono text-base-content/70 placeholder:text-base-content/25 outline-none"
/>
</div>
</div>
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
{#each categories as cat}
{@const catNotes = notes.filter(
(n) => getCategory(n) === cat && matchesSearch(n),
)}
{#if catNotes.length > 0 || !search}
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
<div>
<div class="flex items-center w-full">
<button
onclick={() => toggle(cat)}
class="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 shrink-0"
>
<svg
class="w-3 h-3 text-base-content/35 shrink-0 transition-transform duration-200"
class:rotate-90={openCategories.includes(cat)}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m9 18 6-6-6-6" />
</svg>
<span class="text-primary/50 font-mono text-xs shrink-0">/</span>
</button>
{#if isFolder}
<a
href={`/notes/${cat}`}
class="flex-1 min-w-0 px-1 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 font-bold tracking-tight text-sm truncate text-base-content/80 hover:text-base-content"
>
{cat}
</a>
{:else}
<span class="flex-1 min-w-0 px-1 py-1 font-bold tracking-tight text-sm truncate text-base-content/80">
{cat}
</span>
{/if}
</div>
{#if openCategories.includes(cat)}
<ul
class="ml-4 mt-0.5 pb-1 space-y-px"
transition:slide={{ duration: 180 }}
>
{#each catNotes as note}
<li class="tooltip tooltip-right w-full" data-tip={note.data.title}>
<a
href={`/notes/${note.id}`}
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-mono truncate transition-colors duration-150
{currentEntry && note.id === currentEntry.id
? 'text-primary/90 bg-primary/10'
: 'text-base-content/45 hover:text-base-content/80 hover:bg-base-200/40'}"
>
<span class="shrink-0 font-mono text-base-content/20">
{currentEntry && note.id === currentEntry.id ? "▸" : "–"}
</span>
<span class="truncate">{note.data.title}</span>
</a>
</li>
{/each}
</ul>
{/if}
</div>
{/if}
{/each}
</nav>
</div>
-26
View File
@@ -1,26 +0,0 @@
---
import NoteNavContent from "./NoteNavContent.svelte";
import type { CollectionEntry } from "astro:content";
interface Props {
notes: CollectionEntry<"notes">[];
currentEntry?: CollectionEntry<"notes">;
currentCategory?: string;
categories: string[];
}
const { notes, currentEntry, currentCategory, categories } = Astro.props;
---
<aside
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-full"
style="background: oklch(4% 0 0);"
>
<NoteNavContent
client:load
notes={notes}
currentEntry={currentEntry}
currentCategory={currentCategory}
categories={categories}
/>
</aside>
-40
View File
@@ -1,40 +0,0 @@
---
interface Props {
headings: { depth: number; text: string; id: string }[];
}
const { headings } = Astro.props;
---
{
headings.length > 0 && (
<div
class="collapse collapse-arrow mb-8 border border-base-300/40"
style="background: oklch(4% 0 0);"
>
<input type="checkbox" />
<div class="collapse-title font-mono text-xs text-base-content/35 flex items-center gap-2 py-2 px-3 min-h-0">
<span class="text-primary/40">§</span>
table of contents
</div>
<div class="collapse-content px-0 pb-0">
<nav class="px-3 pb-3 pt-1 border-t border-base-300/30 space-y-0.5">
{headings.map((h) => (
<a
href={`#${h.id}`}
class:list={[
"block text-xs text-base-content/45 hover:text-base-content/80 transition-colors py-0.5",
h.depth === 3 ? "pl-4" : h.depth === 4 ? "pl-8" : "",
]}
>
<span class="font-mono text-primary/25 mr-1.5">
{"#".repeat(h.depth)}
</span>
{h.text}
</a>
))}
</nav>
</div>
</div>
)
}
-120
View File
@@ -1,120 +0,0 @@
<script lang="ts">
import { onMount, untrack } from "svelte";
interface Props {
vars: string[];
}
const { vars }: Props = $props();
let values = $state<Record<string, string>>(
untrack(() => Object.fromEntries(vars.map((v) => [v, ""]))),
);
let open = $state(false);
let applied = $state(false);
const originals = new Map<Text, string>();
onMount(() => {
const content = document.querySelector(".note-content");
if (!content) return;
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
let node: Text | null;
while ((node = walker.nextNode() as Text | null)) {
if (/\$[a-zA-Z_][a-zA-Z0-9_]*/.test(node.nodeValue ?? "")) {
originals.set(node, node.nodeValue!);
}
}
});
function applyVars() {
const sorted = [...vars].sort((a, b) => b.length - a.length);
for (const [node, original] of originals) {
let text = original;
for (const name of sorted) {
const val = values[name];
if (val) {
text = text.replace(
new RegExp(`\\$${name}(?![a-zA-Z0-9_])`, "g"),
val,
);
}
}
node.nodeValue = text;
}
applied = true;
setTimeout(() => (applied = false), 1800);
}
</script>
{#if vars.length > 0}
<button
onclick={() => (open = true)}
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
<span class="font-mono text-[10px] leading-none">$</span>
vars
</button>
{#if open}
<dialog class="modal modal-open">
<div
class="modal-box max-w-sm"
style="background: oklch(10% 0 0); border: 1px solid oklch(71% 0.0863 296.59 / 0.5);"
>
<h3
class="font-mono text-[10px] text-base-content/40 uppercase tracking-widest mb-4"
>
variables
</h3>
<div class="space-y-2.5">
{#each vars as v}
<div class="flex items-center gap-3">
<label
for={`var-${v}`}
class="font-mono text-xs text-primary/70 w-36 shrink-0 truncate"
title={`$${v}`}
>
${v}
</label>
<input
id={`var-${v}`}
type="text"
bind:value={values[v]}
placeholder={`$${v}`}
class="input input-sm flex-1 min-w-0 font-mono text-xs bg-base-300/20 border-base-300/60 text-base-content/80 placeholder:text-base-content/25 focus:border-primary/60"
/>
</div>
{/each}
</div>
<div class="mt-5 flex items-center justify-between">
<span
class="font-mono text-[10px] text-primary/60 transition-opacity duration-300"
class:opacity-0={!applied}
>
✓ applied
</span>
<div class="flex gap-2">
<button
onclick={applyVars}
class="btn btn-ghost btn-xs font-mono text-primary/60 hover:text-primary border border-primary/30"
>
apply
</button>
<button
onclick={() => (open = false)}
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
close
</button>
</div>
</div>
</div>
<button
onclick={() => (open = false)}
class="modal-backdrop"
aria-label="close"
></button>
</dialog>
{/if}
{/if}
-25
View File
@@ -1,25 +0,0 @@
---
import { ArrowRight } from "@lucide/astro";
---
<section id="notes" class="py-10 px-4">
<div class="max-w-6xl mx-auto text-center">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Infosec Notes</h2>
</div>
<div class="mt-8">
<p class="text-base text-base-content/70 mb-6">
Cheatsheets and references on tools and techniques I use for CTFs and
pentesting.
</p>
<div class="text-center mt-12">
<a href="/notes" class="btn btn-ghost gap-2">
Browse Notes
<ArrowRight class="size-4" />
</a>
</div>
</div>
</div>
</section>
-150
View File
@@ -1,150 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
interface NoteItem {
id: string;
title: string;
description: string;
tags: string[];
category: string;
searchText: string;
}
interface Props {
notes: NoteItem[];
}
const { notes }: Props = $props();
let inputValue = $state("");
const query = $derived(inputValue.toLowerCase().trim());
const categories = $derived([
...new Set(notes.map((n) => n.category)),
].sort());
const filtered = $derived.by(() => {
if (!query) return notes;
const isTag = query.startsWith("#");
const q = isTag ? query.slice(1) : query;
return notes.filter((n) =>
isTag
? n.tags.some((t) => t.includes(q)) || n.searchText.includes(`#${q}`)
: n.searchText.includes(q),
);
});
const visibleCount = $derived(filtered.length);
onMount(() => {
const urlTag = new URLSearchParams(window.location.search).get("tag");
if (urlTag) inputValue = `#${urlTag}`;
});
$effect(() => {
const url = new URL(window.location.href);
if (query.startsWith("#") && query.length > 1) {
url.searchParams.set("tag", query.slice(1));
} else {
url.searchParams.delete("tag");
}
history.replaceState(null, "", url.toString());
});
function filteredByCategory(cat: string) {
return filtered.filter((n) => n.category === cat);
}
</script>
<div class="mb-12 max-w-sm mx-auto">
<label class="input w-full">
<span class="text-base-content/25">›</span>
<input
type="text"
placeholder="search or #tag..."
bind:value={inputValue}
/>
</label>
<p class="font-mono text-[10px] text-base-content/20 mt-1.5 text-center">
use #tag to filter by tag
</p>
</div>
<div class="space-y-12">
{#each categories as cat}
{@const catNotes = filteredByCategory(cat)}
{#if catNotes.length > 0}
<section>
<div class="flex items-baseline gap-3 mb-4">
<h2 class="text-xl font-bold tracking-tight">
<span class="text-primary/50 font-mono mr-1">/</span>{cat}
</h2>
<span class="font-mono text-xs text-base-content/25">
{catNotes.length} note{catNotes.length !== 1 ? "s" : ""}
</span>
</div>
<div class="border-t border-base-300/40 mb-1"></div>
<ul class="divide-y divide-base-300/20">
{#each catNotes as n}
<li>
<a
href={`/notes/${n.id}`}
class="group flex items-center gap-4 py-3 hover:bg-base-200/30 px-2 -mx-2 transition-colors"
>
<div class="flex-1 min-w-0">
<div class="flex flex-col mb-0.5">
<span
class="font-semibold text-sm group-hover:text-primary transition-colors"
>
{n.title}
</span>
{#if n.description}
<span class="text-xs text-base-content/35 truncate">
{n.description}
</span>
{/if}
</div>
{#if n.tags.length > 0}
<div class="flex flex-wrap gap-1 mt-1">
{#each n.tags as tag}
<span
class="badge badge-ghost badge-xs font-mono text-base-content/30"
>
{tag}
</span>
{/each}
</div>
{/if}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-base-content/20 group-hover:text-primary/50 shrink-0 transition-colors"
>
<path d="m9 18 6-6-6-6" />
</svg>
</a>
</li>
{/each}
</ul>
</section>
{/if}
{/each}
</div>
{#if visibleCount === 0}
<div class="text-center py-20 font-mono text-sm text-base-content/25">
no results.
</div>
{/if}
<p class="text-center font-mono text-xs text-base-content/20 mt-16">
{visibleCount} note{visibleCount !== 1 ? "s" : ""} total
</p>
+1 -1
View File
@@ -1,5 +1,4 @@
---
---
<script is:inline>
@@ -12,6 +11,7 @@
}
}
document.addEventListener("astro:page-load", initOneko);
initOneko();
</script>
<style is:global>
-30
View File
@@ -1,30 +0,0 @@
---
import GiteaProjectCard from "./GiteaProjectCard.astro";
import { ArrowRight } from "@lucide/astro";
import repos from "../data/repos.json";
const latestRepos = repos.slice(0, 3);
---
<section id="projects" class="py-20 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-4xl font-bold mb-4">Check out my latest work</h2>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{
latestRepos.map((repo) => (
<GiteaProjectCard displayBanner={false} repo={repo} />
))
}
</div>
<div class="text-center mt-12">
<a href="/projects" class="btn btn-ghost gap-2">
View All Projects
<ArrowRight class="size-4" />
</a>
</div>
</div>
</section>
-9
View File
@@ -1,9 +0,0 @@
---
interface Props {
tag: string;
}
const { tag } = Astro.props;
---
<span class="badge badge-sm rounded-sm badge-soft badge-accent">{tag}</span>
-52
View File
@@ -1,52 +0,0 @@
/**
* Social media links configuration
*/
export interface SocialLinks {
github?: string;
gitlab?: string;
gitea?: string;
linkedin?: string;
twitter?: string;
bluesky?: string;
instagram?: string;
youtube?: string;
codetips?: string;
kofi?: string;
medium?: string;
}
/**
* Main site configuration interface
*/
export interface SiteConfig {
name: string;
title: string;
description: string;
avatar: string;
socialLinks: SocialLinks;
gpgKey?: string;
rssFeed?: string;
}
/**
* Site configuration object
* Update these values to customize your portfolio
*/
export const siteConfig: SiteConfig = {
name: "Hadi",
title: "@anotherhadi - Infosec engineer.",
description:
"Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧",
avatar: "/avatar.png",
socialLinks: {
github: "https://github.com/anotherhadi",
gitlab: "https://gitlab.com/anotherhadi_mirror",
gitea: "https://git.hadi.icu",
twitter: "",
bluesky: "",
kofi: "https://ko-fi.com/anotherhadi",
medium: "https://medium.com/@anotherhadi",
},
gpgKey: "/anotherhadi.asc",
rssFeed: "https://hadi.icu/rss.xml",
};
-31
View File
@@ -1,31 +0,0 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
image: image(),
publishDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).optional(),
}),
});
const notes = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/notes" }),
schema: z.object({
title: z.string(),
description: z.string(),
category: z.string().optional(),
tags: z.array(z.string()).default([]),
publishDate: z.coerce.date(),
}),
});
export const collections = {
blog,
notes,
};
-57
View File
@@ -1,57 +0,0 @@
---
title: "The Password is 'admin': Why Default Credentials Are Still Breaking the Internet"
description: "Default credentials like admin:admin remain one of the most exploited vulnerabilities on the internet. Learn why they're dangerous, how the Mirai botnet took down half the web with just 62 passwords, and how to protect your infrastructure: plus introducing default-creds, an open-source database to look up factory-set credentials in seconds."
image: "../../../public/images/blog/default-passwords.png"
tags: ["botnet", "passwords", "cybersecurity"]
publishDate: "2026-03-13"
---
## What are default credentials?
When a manufacturer ships a router, a camera, or a piece of software, it needs to be accessible out of the box. To make **setup easier**, they pre-configure it with a username and password, often something simple like admin/admin or root/password. These are called **default credentials**.
_The problem?_ Most users never change them. Whether out of convenience, lack of awareness, or simply because the service "works fine as-is", these factory-set credentials often remain active long after deployment.. turning a minor convenience into a serious **security hole**.
To help security researchers and pentesters quickly identify these exposure points, I built **[default-creds](https://default-creds.hadi.icu)**. It's an open-source, community-driven database of default credentials. Just search for a device or service, and you'll instantly get its known factory-set username and password. It also comes with a public API, documented at [default-creds.hadi.icu/api-docs](https://default-creds.hadi.icu/api-docs).
## Real-world impact
The consequences of unchanged default credentials aren't theoretical: they've already broken the internet, literally.
In the fall of 2016, a piece of malware called **Mirai** quietly scanned the internet for IoT devices still running their factory-set credentials. Using a list of just 62 common default username/password combinations like `admin:admin` or `root:password`, it managed to enslave over 380,000 devices (mostly routers, IP cameras, DVRs, ...) and turning them into an army.
On September 20, 2016, Brian Krebs' security blog was hit with a DDoS attack exceeding 620 Gbps, one of the largest ever recorded at the time. Then came the attack on French web host OVH, which broke that record. And then, in October 2016, Mirai took down **Dyn** (a major DNS provider) causing disruptions to Twitter, Spotify, Amazon, Netflix, GitHub, and PayPal, among others, with attacks reportedly peaking at 1 Tbps.
All of this, enabled by `admin:admin`.
Mirai wasn't a sophisticated zero-day exploit. It was a dictionary of 62 passwords. The attack surface wasn't a vulnerability in the code; it was human laziness at scale.
The original Mirai and its early variants launched approximately 20,000 DDoS attacks between late 2016 and early 2017. And even though its creators were eventually arrested, the source code lives on, having spawned numerous variants that continue to operate today.
Default credentials aren't just a consumer problem. Enterprises, developers, and sysadmins are equally exposed; From home routers and IP cameras to network switches, firewalls, and self-hosted services like Grafana, Redis, or Jenkins. If it has a login screen and was deployed without changing the defaults, it's a target.
## Best practices & solutions
### For users & sysadmins
1. **Change default credentials immediately.** The moment you deploy a new device or service, changing the default username and password should be the first thing you do; before it ever touches a production network.
2. **Use strong, unique passwords.** Replacing `admin:admin` with `admin:admin123` doesn't count. Use a password manager to generate and store proper credentials for each service.
3. **Audit your infrastructure.** You can't fix what you don't know about. Regularly scan your own systems for services still running on default credentials: this is exactly the kind of task [default-creds](https://default-creds.hadi.icu/) is built for.
### For developers
1. **Never ship with hardcoded default credentials.** A default password baked into your codebase is a vulnerability waiting to be exploited (and it will end up in databases like [default-creds](https://default-creds.hadi.icu) :p )
2. **Force a password change on first launch.** If your software needs a default to function, make it temporary. Block access until the user has set their own credentials.
3. **Generate a random password instead.** Even better: skip the default entirely. Generate a strong, unique password at install time and print it once in the console or the setup logs. The user still should change this password.
## How to contribute
**default-creds** is only as useful as its data. If you know a device or service that's missing from the database, contributing is straightforward. The project is open-source on [GitHub](https://github.com/anotherhadi/default-creds) under the MIT license, and contributions are made via Pull Requests by adding simple YAML definitions.
The full contribution guide is available in the [CONTRIBUTING.md](https://github.com/anotherhadi/default-creds/blob/main/CONTRIBUTING.md).
---
If you enjoyed this guide, please like and share it! Your support helps me create more infosec & OSINT content.
Have questions or feedback? Feel free to reach out: anotherhadi.clapped234[at]passmail.net
-135
View File
@@ -1,135 +0,0 @@
---
title: "Unmasking Github Users: How to Identify the Person Behind Any Github Profile"
description: "Ever wondered who is behind a specific Github username? This guide covers advanced OSINT techniques to deanonymize users, find hidden email addresses, and link Github accounts to real-world identities."
image: "../../../public/images/blog/github-osint-users.png"
tags: ["osint", "github", "cybersecurity"]
publishDate: "2026-01-01"
---
In the world of Open-Source Intelligence (OSINT), we often focus on social media platforms like Twitter or LinkedIn. However, developers frequently leave behind much more detailed personal information on **Github**.
Whether you are a recruiter, a security researcher, or a digital investigator, Github is a goldmine. Why? Because while a user might choose a cryptic handle like `anotherhadi`, their Git configuration often reveals their real name and email address.
## Level 1: The Low-Hanging Fruit
Before diving into technical exploits, start with the obvious. Many users forget how much they have shared in their profile settings.
- **The Bio & Location**: Even a vague location like "Montpellier, France," combined with a niche tech stack (e.g., "COBOL expert"), significantly narrows down the search.
- **External Links**: Check the personal website or blog link. Run a WHOIS lookup on that domain to find registration details. Use other OSINT tools and techniques on those websites to pivot further.
- **The Profile Picture**: Right-click the avatar and use Google Reverse Image Search, Yandex, or other reverse image engines. Developers often use the same professional headshot on Github as they do on LinkedIn.
## Level 2: Digging into Commits
This is the **most effective OSINT** method. While Github masks author names and emails in the web view, this information is permanently embedded in the commit metadata.
### The `.patch` Method
Find a repository where the target has contributed. Open any commit they made, and simply add `.patch` to the end of the URL.
- **URL**: `https://github.com/{username}/{repo}/commit/{commit_hash}.patch`
- Look at the `From:` line. It should look like this: `From: John Doe <[email protected]>`
For example, check: [github.com/anotherhadi/nixy/commit/e6873e8caae491073d8ab7daad9d2e50a04490ce.patch](https://github.com/anotherhadi/nixy/commit/e6873e8caae491073d8ab7daad9d2e50a04490ce.patch)
### The API Events Method
If you cannot find a recent commit, check their **public activity** stream via the Github API.
- **Go to**: `https://api.github.com/users/{target_username}/events/public`
- Search (Ctrl+F) for the word `email`. You will often find the **email address** associated with their `PushEvent` headers, even if they have "Keep my email addresses private" enabled in their current settings.
## The Verification Loop: Linking Email to Account
If you have found an email address and want to be 100% sure it belongs to a specific Github profile, you can use Github’s own attribution engine against itself.
### The Email Spoofing Method
While the previous methods help you find an email _from_ a profile, this technique does the opposite: it identifies which Github account is linked to a specific email address.
**How it works:**
Github attributes commits based on the email address found in the Git metadata. If you push a commit using a specific email, Github will automatically link that commit to the account associated with that address as its **primary email**.
**The Process:**
1. **Initialize a local repo:** `git init investigation`
2. **Configure the target email:** `git config user.email "[email protected]"` and `git config user.name "A Username"`
3. **Create a dummy commit:** `echo "test" > probe.txt && git add . && git commit -m "Probe"`
4. **Push to a repo you own:** Create a new empty repository on your Github account and push the code there.
5. **Observe the result:** Go to the commit history on the Github web interface. The avatar and username of the account linked to that email will appear as the author of the commit.
> **Note:** This method only works if the target email is set as the **Primary Email** on the user's account. It is a foolproof way to confirm if an email address you found elsewhere belongs to a specific Github user.
### The Search Index: Finding Hidden Contributions
Even if an email address is not listed on a user's profile, it may still be indexed within Github's global search.
Github allows you to filter search results by the metadata fields of a commit.
This is particularly useful if the target has **contributed to public repositories** using their real email.
You can use these specific qualifiers in the **Github search bar** (select the "Commits" tab):
- `author-email:[email protected]`: Finds commits where the target is the original author.
- `committer-email:[email protected]`: Finds commits where the target was the one who committed the code (sometimes different from the author).
## Level 3: Technical Metadata
If the email is masked or missing, we can look at the **cryptographic keys** the user uses to communicate with Github.
### SSH Keys
Every user’s public **SSH keys are public**.
- **URL**: `https://github.com/{username}.keys`
- **The Pivot**: You can take the key string and search for it on platforms like **Censys** or **Shodan**. If that same key is authorized on a specific server IP, you have successfully located the user’s infrastructure.
### GPG Keys
If a user signs their commits, their **GPG key** is available at:
- **URL**: `https://github.com/{username}.gpg`
- **The Reveal**: Import this key into your local GPG tool (`gpg --import`). It will often reveal the **Verified Identity** and the primary email address linked to the encryption key.
## Level 4: Connecting the Dots
Once you have a **name**, an **email**, or a **unique username**, it’s time to _pivot_.
- **Username Pivoting**: Use tools like [Sherlock](https://github.com/sherlock-project/sherlock) or [Maigret](https://github.com/soxoj/maigret/) to search for the same username across hundreds of other platforms. Developers are creatures of habit; they likely use the same handle on Stack Overflow, Reddit, or even old gaming forums.
- **Email Pivoting**: Use tools like [holehe](https://github.com/megadose/holehe) to find other accounts registered with the email addresses you just uncovered.
## Automating the Hunt: Github-Recon
If you want to move from manual investigation to automated intelligence, check out [Github-Recon](https://github.com/anotherhadi/github-recon).
Written in Go, this powerful CLI tool aggregates public OSINT data by automating the techniques mentioned above and more. Whether you start with a username or a single email address, it can retrieve SSH/GPG keys, enumerate social accounts, and find "close friends" based on interactions.
Its standout features include a **Deep Scan** mode-which clones repositories to perform regex searches and TruffleHog secret detection—and an automated **Email Spoofing** engine that instantly identifies the account linked to any primary email address.
## Conclusion and Protection: How to Stay Anonymous
If you are a developer reading this, you might be feeling exposed.
Understanding what information about you is publicly visible is the **first step to managing your online presence**. This guide and tools like [github-recon](https://github.com/anotherhadi/github-recon) can help you identify your own publicly available data on Github. Here’s how you can take steps to protect your privacy and security:
- **Review your public profile**: Regularly check your Github profile and
repositories to ensure that you are not unintentionally exposing sensitive
information.
- **Manage email exposure**: Use Github's settings to control which email
addresses are visible on your profile and in commit history. You can also **use
a no-reply email** address for commits, and an
[alias email](https://proton.me/support/addresses-and-aliases) for your
account. Delete/modify any sensitive information in your commit history.
- **Be Mindful of Repository Content**: **Avoid including sensitive information** in
your repositories, such as API keys, passwords, emails or personal data. Use
`.gitignore` to exclude files that contain sensitive information.
You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog)
to scan your repositories specifically for exposed secrets and tokens.
**Useful links:**
- [Blocking command line pushes that expose your personal email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/blocking-command-line-pushes-that-expose-your-personal-email-address)
- [No-reply email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
In OSINT, the best hidden secrets are the ones we forget we ever shared. Happy hunting!
---
If you enjoyed this guide, please like and share it! Your support helps me create more infosec & OSINT content.
Have questions or feedback? Feel free to reach out: anotherhadi.clapped234[at]passmail.net
-67
View File
@@ -1,67 +0,0 @@
---
title: "GRUB Boot Bypass"
description: "Physical access techniques to get a root shell by editing GRUB boot parameters."
tags: ["linux", "grub", "physical-access", "privesc"]
publishDate: 2026-05-18
---
When GRUB is not password-protected, anyone with physical access can edit boot parameters and bypass authentication entirely.
At the GRUB menu, press **`e`** to edit the selected entry. Modify the line starting with `linux`, then press **`F10`** to boot.
## Techniques
### init=/bin/sh
Replaces the init process with a shell; drops directly into a root shell before any login prompt.
```
linux ... init=/bin/sh
```
Filesystem is mounted read-only by default. Remount to make changes:
```bash
mount -o remount,rw /
```
### init=/bin/bash
Same as above but uses bash. Add `rw` on the `linux` line to mount read-write from the start:
```
linux ... rw init=/bin/bash
```
### rd.break (systemd)
Interrupts the boot process in the initramfs, before the real root filesystem is mounted. Useful for resetting the root password.
```
linux ... rd.break
```
From the initramfs shell:
```bash
mount -o remount,rw /sysroot
chroot /sysroot
passwd root
exit
```
### single (single-user mode)
Boots into maintenance mode. On some distros this drops to a root shell without a password prompt (not Debian/Ubuntu).
```
linux ... single
```
### systemd.unit=rescue.target
systemd equivalent of single-user mode: minimal services, root shell.
```
linux ... systemd.unit=rescue.target
```
-80
View File
@@ -1,80 +0,0 @@
---
title: "Linux Privilege Escalation"
description: "Common misconfigurations and weaknesses to check when escalating privileges on Linux."
tags: ["linux", "privesc", "post-exploitation"]
publishDate: 2026-05-18
---
## Sudo
```bash
sudo -l
```
Check [GTFOBins](https://gtfobins.github.io) for any listed binary.
If `env_keep+=LD_PRELOAD` is set:
```bash
# compile a shared lib that spawns a shell
gcc -fPIC -shared -o /tmp/shell.so shell.c -nostartfiles
sudo LD_PRELOAD=/tmp/shell.so <allowed_binary>
```
## SUID / SGID
```bash
find / -user root -perm -4000 -ls 2>/dev/null # SUID
find / -group root -perm -2000 -ls 2>/dev/null # SGID
```
Check any non-standard binary on GTFOBins.
## Misconfiguration
```bash
# World-writable directories
find / -type d -perm -2 -ls 2>/dev/null
# World-writable files owned by root
find / -user root -perm -2 ! -type l -ls 2>/dev/null
```
## Cron Jobs
```bash
cat /etc/crontab
ls -la /etc/cron.*
crontab -l
```
If a cron runs a script you can write to, replace its content:
```bash
echo 'chmod +s /bin/bash' >> /path/to/script.sh
```
If the cron uses a relative PATH and a directory is writable, drop a malicious binary earlier in `$PATH`.
## Capabilities
```bash
getcap -r / 2>/dev/null
```
Dangerous capabilities: `cap_setuid`, `cap_net_raw`, `cap_dac_override`.
Check [GTFOBins](https://gtfobins.github.io) for exploitation.
## Kernel Exploits
```bash
uname -r
searchsploit linux kernel $(uname -r)
```
## LinPEAS / WinPEAS
Automated enumeration scripts to surface privesc vectors quickly.
- [LinPEAS (linux)](https://github.com/peass-ng/PEASS-ng/tree/master/linPEAS)
- [WinPEAS (windows)](https://github.com/peass-ng/PEASS-ng/tree/master/winPEAS)
-60
View File
@@ -1,60 +0,0 @@
---
title: "FTP"
description: "Enumeration, exploitation and post-exploitation techniques for FTP servers."
tags: ["ftp", "network", "service"]
publishDate: 2026-04-29
---
FTP runs on **port 21** (control) and uses a secondary data channel (port 20 for active, ephemeral port for passive).
Common implementations: vsftpd, ProFTPD, Pure-FTPd, FileZilla Server, IIS FTP.
## Enumeration
### Banner grabbing
```bash
nc -nv $IP 21
ftp $IP
```
The banner often reveals the software version: cross-reference with CVE databases.
### Nmap
```bash
nmap -sV -p 21 $IP
nmap -p 21 --script ftp-* $IP
```
Key scripts:
- `ftp-anon`: checks anonymous login
- `ftp-bounce`: tests for FTP bounce attack
- `ftp-brute`: brute-force credentials
- `ftp-syst`: retrieves system info
## Anonymous Login
```bash
ftp $IP
# Username: anonymous
# Password: <empty> or anonymous@
```
If allowed, list and download everything:
```bash
ls -la
mget *
```
Check for writable directories: you may be able to upload a webshell if FTP root overlaps with a web root.
## Brute Force
```bash
hydra -l $user -P ~/wordlists/rockyou.txt ftp://$IP
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M ftp
```
Try default credentials first: `admin:admin`, `ftp:ftp`, `user:password`.
-79
View File
@@ -1,79 +0,0 @@
---
title: "NFS"
description: "Enumeration, mounting and privilege escalation techniques for NFS shares."
tags: ["nfs", "network", "service"]
publishDate: 2026-05-18
---
NFS (Network File System) runs on **port 2049** and allows remote filesystem mounting over the network.
Common on Linux/Unix environments. Access control is defined in `/etc/exports` on the server.
## Enumeration
### Nmap
```bash
nmap -sV -p 111,2049 $IP
nmap -p 111,2049 --script nfs-* $IP
```
Key scripts:
- `nfs-showmount`: lists exported shares
- `nfs-ls`: lists files in shares
- `nfs-statfs`: retrieves disk stats
### List shares
```bash
showmount -e $IP
rpcinfo -p $IP
```
## Mount
```bash
mkdir /mnt/nfs
mount -t nfs $IP:/share /mnt/nfs
mount -t nfs -o vers=2 $IP:/share /mnt/nfs # force NFSv2
umount /mnt/nfs
```
## Privilege Escalation
### no_root_squash
If the share is exported with `no_root_squash`, the remote root user keeps root privileges on the share.
Check `/etc/exports` on the server (if readable):
```bash
cat /etc/exports
```
Look for:
```
/share *(rw,no_root_squash)
```
If present, copy a SUID binary onto the share as root from your attacker machine:
```bash
cp /bin/bash /mnt/nfs/bash
chmod +s /mnt/nfs/bash
```
Then execute it on the target with `-p` to keep the SUID effective UID:
```bash
/tmp/nfs/bash -p
```
### UID spoofing
NFS authenticates by UID. If you know a file is owned by UID 1001 on the server, impersonate it directly:
```bash
python3 -c "import os; os.setuid(1001); os.system('/bin/bash')"
```
-108
View File
@@ -1,108 +0,0 @@
---
title: "Nmap"
description: "Host discovery, port scanning, service detection and NSE scripting"
tags: ["nmap", "network", "enumeration"]
publishDate: 2026-05-18
---
Nmap is a network scanner used for host discovery, port scanning, service/version detection, OS fingerprinting, and vulnerability scripting via NSE.
## Host Discovery
```bash
nmap -sn 192.168.1.0/24 # ping sweep, no port scan
nmap -sn -PR 192.168.1.0/24 # ARP ping (local network)
nmap -Pn $IP # skip host discovery, treat as up
```
## Port Scanning
```bash
nmap $IP # top 1000 ports (SYN scan if root)
nmap -p 80,443,8080 $IP # specific ports
nmap -p 1-65535 $IP # all ports
nmap -p- $IP # shorthand for all ports
nmap -sU $IP # UDP scan
nmap -sU -sS $IP # UDP + SYN together
```
Scan types:
- `-sS`: SYN scan (stealth, requires root)
- `-sT`: TCP connect scan (no root needed)
- `-sU`: UDP scan
- `-sA`: ACK scan (firewall rule mapping)
- `-sN/sF/sX`: Null, FIN, Xmas (evasion, unreliable on Windows)
## Service & Version Detection
```bash
nmap -sV $IP
nmap -sV --version-intensity 9 $IP # more aggressive probing
```
## OS Detection
```bash
nmap -O $IP
nmap -O --osscan-guess $IP # guess if not confident
```
## Aggressive Scan
```bash
nmap -A $IP # -sV -O --script=default --traceroute
```
## Timing Templates
```bash
nmap -T0 $IP # paranoid (IDS evasion, very slow)
nmap -T1 $IP # sneaky
nmap -T3 $IP # normal (default)
nmap -T4 $IP # aggressive (faster, good for CTFs)
nmap -T5 $IP # insane (may miss results)
```
## NSE Scripts
```bash
nmap --script default $IP
nmap --script vuln $IP
nmap --script "ftp-*" $IP
nmap --script safe $IP
nmap --script $script --script-args user=$user,pass=$password $IP
```
Common script categories: `auth`, `brute`, `default`, `discovery`, `dos`, `exploit`, `intrusive`, `safe`, `version`, `vuln`.
Scripts are located in `/usr/share/nmap/scripts/`.
## Output Formats
```bash
nmap -oN output.txt $IP # normal
nmap -oX output.xml $IP # XML
nmap -oG output.gnmap $IP # grepable
nmap -oA output $IP # all three at once
```
## Common Profiles
Quick full scan:
```bash
nmap -p- -T4 --min-rate 5000 -sV -sC -oA full $IP
```
CTF/lab initial recon:
```bash
nmap -sV -sC -p- --open $IP
```
UDP top ports:
```bash
nmap -sU --top-ports 100 $IP
```
-44
View File
@@ -1,44 +0,0 @@
---
title: "RDP"
description: "Enumeration, exploitation and post-exploitation techniques for RDP servers."
tags: ["rdp", "network", "service"]
publishDate: 2026-05-04
---
RDP (Remote Desktop Protocol) runs on **port 3389** and provides a graphical remote session.
Common on Windows servers and workstations.
## Enumeration
### Banner grabbing
```bash
nmap -sV -p 3389 $IP
nmap -p 3389 --script rdp-* $IP
```
Key scripts:
- `rdp-enum-encryption`: checks encryption level
- `rdp-vuln-ms12-020`: tests for MS12-020 DoS vulnerability
## Connect
```bash
xfreerdp /u:$user /p:$password /v:$IP
xfreerdp /u:$user /p:$password /v:$IP /cert:ignore
rdesktop $IP
```
Pass the hash directly (no plaintext password needed):
```bash
xfreerdp /u:$user /pth:$hash /v:$IP
```
## Brute Force
```bash
hydra -l $user -P ~/wordlists/rockyou.txt rdp://$IP
crowbar -b rdp -s $IP/32 -u $user -C ~/wordlists/rockyou.txt
```
-73
View File
@@ -1,73 +0,0 @@
---
title: "SSH"
description: "Enumeration, exploitation and post-exploitation techniques for SSH servers."
tags: ["ssh", "network", "service"]
publishDate: 2026-05-04
---
SSH runs on **port 22** and provides an encrypted remote shell.
Common implementations: OpenSSH, Dropbear, Bitvise.
## Enumeration
### Banner grabbing
```bash
nc -nv $IP 22
ssh $IP
```
The banner reveals the software and version (e.g. `OpenSSH_9.2`).
### Nmap
```bash
nmap -sV -p 22 $IP
nmap -p 22 --script ssh-* $IP
```
Key scripts:
- `ssh-hostkey`: retrieves the server's public key
- `ssh-auth-methods`: lists accepted authentication methods
- `ssh-brute`: brute-force credentials
## Connect
```bash
ssh $user@$IP
ssh -p 2222 $user@$IP
ssh -i id_rsa $user@$IP
```
## Brute Force
```bash
hydra -l $user -P ~/wordlists/rockyou.txt ssh://$IP
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M ssh
```
Only viable if password auth is enabled. Check with:
```bash
ssh -v $user@$IP
```
Look for `publickey,password` in the output.
## Key-Based Auth
If you find a private key (`id_rsa`), set permissions and connect:
```bash
chmod 600 id_rsa
ssh -i id_rsa $user@$IP
```
If the key is encrypted, crack the passphrase:
```bash
ssh2john id_rsa > hash.txt
john hash.txt --wordlist=~/wordlists/rockyou.txt
hashcat -m 22921 hash.txt ~/wordlists/rockyou.txt
```
-50
View File
@@ -1,50 +0,0 @@
---
title: "Telnet"
description: "Enumeration, exploitation and post-exploitation techniques for Telnet servers."
tags: ["telnet", "network", "service"]
publishDate: 2026-05-04
---
Telnet runs on **port 23** and transmits all data (including credentials) in **cleartext**.
Common on embedded devices, legacy systems, routers, and IoT equipment.
## Enumeration
### Banner grabbing
```bash
nc -nv $IP 23
telnet $IP
```
The banner often reveals the OS, hostname, or device type.
### Nmap
```bash
nmap -sV -p 23 $IP
nmap -p 23 --script telnet-* $IP
```
Key scripts:
- `telnet-ntlm-info`: extracts NTLM info (Windows targets)
- `telnet-brute`: brute-force credentials
## Connect
```bash
telnet $IP
telnet $IP 23
```
Login with `user` / `password`. Session is fully interactive once authenticated.
## Brute Force
```bash
hydra -l $user -P ~/wordlists/rockyou.txt telnet://$IP
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M telnet
```
Try default credentials first. Routers and embedded devices commonly ship with `admin:admin`, `root:root`, or blank passwords.
-97
View File
@@ -1,97 +0,0 @@
---
title: "Bluesky"
description: "Enumeration, search operators, API endpoints and tools for investigating Bluesky accounts."
tags: ["osint", "bluesky", "social-media", "enumeration"]
publishDate: 2026-04-29
---
## Key Concepts
Bluesky is built on the **AT Protocol**. Every account has two identifiers:
- **Handle**: `user.bsky.social` or a custom domain (can change)
- **DID**: `did:plc:ewvi7nxzyoun6zhxrhs64oiz` (permanent, survives handle changes)
All public content is accessible **without an account**. Follower/following lists are also public by default.
## Account Enumeration
### Resolve handle → DID
```
https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=$HANDLE
```
### Resolve DID → history (all past handles, keys, creation date)
```
https://plc.directory/$DID
```
### Get profile metadata
```
https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=$HANDLE
```
Returns: DID, display name, description, follower/following count, creation date, avatar URL.
### Followers / following
```
https://public.api.bsky.app/xrpc/app.bsky.graph.getFollowers?actor=$HANDLE&limit=100
https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=$HANDLE&limit=100
```
Paginate with the `cursor` field from the response.
## Search Operators
Bluesky's full-text search supports these operators (combinable):
| Operator | Example | Effect |
| ----------- | ----------------------------- | ----------------------------- |
| `"..."` | `"exact phrase"` | Exact match |
| `from:` | `from:handle.bsky.social` | Posts by user |
| `mentions:` | `mentions:handle.bsky.social` | Posts mentioning user |
| `since:` | `since:2024-01-01` | After date (UTC, YYYY-MM-DD) |
| `until:` | `until:2024-06-30` | Before date (UTC, YYYY-MM-DD) |
| `lang:` | `lang:fr` | Language (ISO 639-1) |
| `domain:` | `domain:github.com` | Posts linking to domain |
| `#tag` | `#osint` | Hashtag |
#### API equivalent
```
https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q={QUERY}&author={HANDLE}&since=2024-01-01&until=2024-12-31&lang=en&limit=25
```
## Google Dorks
Bluesky is heavily indexed by Google. Useful for finding profiles and posts without touching the platform:
```
site:bsky.app "$TARGET_NAME"
site:bsky.app "$TARGET_NAME" inurl:profile
site:bsky.app "$KEYWORD" since:2024-01-01
```
## Tools
### BlueSkyNet
Web app for searching and exporting Bluesky data to CSV. Wraps the public API with a UI for advanced search filters.
- [github.com/jakecreps/blueskynet](https://github.com/jakecreps/blueskynet)
### ClearSky
Shows block lists, blocking history, and who blocked a given account. Useful for mapping relationships and adversarial clusters.
- [clearsky.app](https://clearsky.app)
### plc.directory
Official DID PLC directory. Lookup a DID to get full account history: creation date, all past handles, key rotations.
- [plc.directory](https://plc.directory)
@@ -1,69 +0,0 @@
---
title: "Information Gathering"
description: "Essential cybersecurity cheatsheet for Information Gathering and Open Source Intelligence (OSINT). Discover data related to emails, domains, usernames, and images using both command line and online tools."
tags: ["osint", "enumeration", "information-gathering"]
publishDate: 2026-05-03
---
**Information Gathering**, often referred to as **Open Source Intelligence (OSINT)** in the context of ethical hacking, is the systematic collection and analysis of publicly available data about a target, providing the foundational knowledge necessary to identify potential vulnerabilities and craft targeted security assessments.
## Command line tools
| **From** | **Use** |
| --------- | ----------------------------------------------------------------------------------------------- |
| Email | `holehe $email` |
| | `ghunt email $email` (for google account) |
| | `github-recon $email` ([link](http://github.com/anotherhadi/github-recon/), for github account) |
| Domain | `theHarvester -d $domain -l 100` |
| | `theHarvester -d $domain -l 100 -b all` (full) |
| Username | `sherlock $username` |
| Image | `exiftool $imagePath` |
| Instagram | `instaloader profile $username` |
| Github | `trufflehog github --org=$usernameOrOrg` |
| | `github-recon $username` ([link](http://github.com/anotherhadi/github-recon/)) |
## Online tools
| **For** | **Use** |
| ---------- | ------------------------------------------------------ |
| Visualiser | [OSINTracker](https://www.osintracker.com/) |
| IP | [Shodan](https://www.shodan.io/) |
| | [Censys](https://search.censys.io/) |
| Domain | [Whois](https://www.whois.com/whois/) |
| | [crt.sh](https://crt.sh/) (certificate transparency) |
| Name | [Webmii](https://webmii.com/) |
| | [BreachDirectory](https://breachdirectory.org/) |
| | [LeakLookup](https://leak-lookup.com/search) |
| | [IntelX](https://intelx.io/) |
| | [Genealogic.review](https://genealogic.review/) |
| SSID | [Wigle](https://wigle.net/) |
| Image | [PimEyes (faces)](https://pimeyes.com/) |
| | [Lenso (faces)](https://lenso.ai) |
| | [TinEye](https://tineye.com) |
| | [Pic2Map (exif geolocation)](https://www.pic2map.com/) |
| Username | [DeHashed](https://dehashed.com/search) |
| | [BreachDirectory](https://breachdirectory.org/) |
| | [IntelX](https://intelx.io/) |
| | [LeakLookup](https://leak-lookup.com/search) |
| | [Oathnet](https://oathnet.org/) |
| Email | [DeHashed](https://dehashed.com/search) |
| | [Hunter](https://hunter.io/) |
| | [HaveIBeenPwned](https://haveibeenpwned.com/) |
| | [BreachDirectory](https://breachdirectory.org/) |
| | [LeakLookup](https://leak-lookup.com/search) |
| | [IntelX](https://intelx.io/) |
| | [Oathnet](https://oathnet.org/) |
| Phone | [Epieos](https://epieos.com/) |
| Instagram | [Dumpor](https://dumpor.io/) |
| Misc | [Goosint](https://goosint.com/) |
| | [OSINT Framework](https://osintframework.com/) |
| | [OSINT Dojo](https://osintdojo.com/) |
## OSINT Aggregation Tool
<a href="https://iknowyou.hadi.icu" class="link-card not-prose" target="_blank">
<span>
<h4>IKnowYou</h4>
<p>Self-hosted OSINT aggregation platform: Run dozens of open-source intelligence tools against a single target in parallel; all from one clean web interface.</p>
</span>
</a>
-83
View File
@@ -1,83 +0,0 @@
---
title: "Sock Puppets"
description: "Essential cheatsheet on creating and managing Sock Puppets (fake identities) for ethical security research and Open Source Intelligence (OSINT), focusing on maintaining separation from personal data and bypassing common verification."
tags: ["osint", "sock-puppets"]
publishDate: 2026-05-03
---
Sock puppets are fake identities use to gather information from a target.
The sock puppet should have no link between your personal information and the fakes ones. (No ip address, mail, follow, etc..)
## Information generation
<a href="https://fakerjs.dev" class="link-card not-prose" target="_blank">
<span>
<h4>Faker</h4>
<p>Generate massive amounts of fake data</p>
</span>
</a>
<a href="https://fakenamegenerator.com/" class="link-card not-prose" target="_blank">
<span>
<h4>Fake Name</h4>
<p>Personal informations</p>
</span>
</a>
<a href="https://www.thispersondoesnotexist.com/" class="link-card not-prose" target="_blank">
<span>
<h4>This Person Does Not Exist</h4>
<p>Generate fake image</p>
</span>
</a>
## Bypass phone verification
<a href="https://www.smspool.net/" class="link-card not-prose" target="_blank">
<span>
<h4>SMSPool</h4>
<p>Cheapest and Fastest Online SMS verification</p>
</span>
</a>
<a href="https://receive-sms-online.info" class="link-card not-prose" target="_blank">
<span>
<h4>Receive Sms Online</h4>
<p>Free SMS verification</p>
</span>
</a>
<a href="https://receivefreesms.net" class="link-card not-prose" target="_blank">
<span>
<h4>Receive Free Sms</h4>
<p>Free SMS verification</p>
</span>
</a>
<a href="https://receive-smss.com" class="link-card not-prose" target="_blank">
<span>
<h4>Receive Free Sms</h4>
<p>Free SMS verification</p>
</span>
</a>
<a href="https://onlinesim.io/" class="link-card not-prose" target="_blank">
<span>
<h4>Online Sim</h4>
<p>SMS verification with free tier</p>
</span>
</a>
<a href="https://sms4stats.com/" class="link-card not-prose" target="_blank">
<span>
<h4>Sms 4 Sats</h4>
<p>Paid SMS verification</p>
</span>
</a>
<a href="http://sms4sat6y7lkq4vscloomatwyj33cfeddukkvujo2hkdqtmyi465spid.onion" class="link-card not-prose" target="_blank">
<span>
<h4>Sms 4 Sats (Onion)</h4>
<p>Paid SMS verification. Tor version</p>
</span>
</a>
-42
View File
@@ -1,42 +0,0 @@
---
title: "Tips"
description: "A cheatsheet of practical tips and unconventional methods for Open Source Intelligence (OSINT), focusing on advanced data visualization, information leakage detection, and utilizing web archives for historical data."
tags: ["osint"]
publishDate: 2026-05-03
---
## Visualisation
Use [OSINTracker](https://app.osintracker.com/) to visualise your findings.
It allows you to create a graph of your findings, which can help you see connections and relationships between different pieces of information.
## Forgotten passwords
To find email addresses and phone numbers associated with an account, you can click on "Forgot password?" on the login page of a website. Be careful, though, this creates notifications and can be detected by the target, and often gives your information away.
## Archive Search
- [Wayback Machine](https://web.archive.org) stores over 618 billion web captures
- [Archive.ph](https://archive.ph) creates on-demand snapshots, including for JS-heavy sites, with both a functional page and screenshot version
## Google Cache
Google keeps a cached version of most indexed pages. Access it with the `cache:` operator:
```
cache:example.com
cache:example.com/page
```
If the page has been taken down or modified, the cached version may still show the original content.
## Domain History
[VirusTotal](https://www.virustotal.com) shows the historical DNS records, subdomains, and associated IPs for any domaint useful when a site has moved or been taken down.
[ViewDNS.info](https://viewdns.info) covers WHOIS history, reverse IP, reverse MX, and port scans from a single interface.
## Bookmarklets
- [K2SOsint/Bookmarklets](https://github.com/K2SOsint/Bookmarklets)
- [tools.myosint.training](https://tools.myosint.training/)
-138
View File
@@ -1,138 +0,0 @@
---
title: "X / Twitter"
description: "Enumeration, search operators, deleted content recovery and tools for investigating X accounts."
tags: ["osint", "twitter", "x", "social-media", "enumeration"]
publishDate: 2026-04-29
---
## Key Concepts
Every account has two identifiers:
- **Handle**: `@username` (can change)
- **User ID**: numeric, permanent (survives handle changes and suspensions)
Unlike [Bluesky](/notes/osint/bluesky), X now requires a login to browse most content in the browser. The free API tier (v2) is severely limited. Most open-source scraping tools that bypassed the API (Twint, snscrape, GetOldTweets3) are broken since the 2023 API lockdown.
## Account Enumeration
### Handle to User ID
The user ID stays constant when someone changes their handle or gets suspended. Several web tools resolve it:
- [tweeterid.com](https://tweeterid.com/)
- [commentpicker.com/twitter-id.php](https://commentpicker.com/twitter-id.php)
Or via the profile page source: look for `"id_str"` in the page JSON.
### Banner last update time
The profile banner URL contains a Unix timestamp indicating when the banner was last changed:
```
https://pbs.twimg.com/profile_banners/{user_id}/{unix_timestamp}/600x200
```
Right-click the banner image and copy the URL, or inspect the page source. Convert the timestamp at [unixtimestamp.com](https://www.unixtimestamp.com/).
### Timestamp from ID (Snowflake)
Twitter IDs are Snowflake IDs: the numeric value encodes the exact creation time of a tweet or account. Extract it with:
```python
tweet_id = 1234567890123456789
timestamp_ms = (tweet_id >> 22) + 1288834974657
```
`1288834974657` is Twitter's custom epoch (Nov 4, 2010). Works on both tweet IDs and user IDs: useful to confirm account creation date without needing profile metadata.
Several online converters exist if you don't want to do it manually: search "snowflake id decoder".
### Direct profile URL by ID
Old tweet/profile URLs using numeric IDs still resolve even after handle changes:
```
https://x.com/i/user/$USER_ID
```
## Search Operators
Accessible at `x.com/search`. Operators are combinable.
| Operator | Example | Effect |
| ----------------- | -------------------------- | ------------------------ |
| `"..."` | `"exact phrase"` | Exact match |
| `from:` | `from:handle` | Posts by user |
| `to:` | `to:handle` | Posts directed at user |
| `since:` | `since:2024-01-01` | After date (YYYY-MM-DD) |
| `until:` | `until:2024-06-30` | Before date (YYYY-MM-DD) |
| `lang:` | `lang:fr` | Language (ISO 639-1) |
| `near:` | `near:"Paris" within:10km` | Geo (web only, not API) |
| `geocode:` | `geocode:48.85,2.35,5km` | Geo by coordinates |
| `filter:images` | | Posts with images |
| `filter:videos` | | Posts with videos |
| `filter:links` | | Posts with URLs |
| `filter:verified` | | Verified accounts only |
| `-filter:replies` | | Exclude replies |
| `min_retweets:` | `min_retweets:100` | Engagement threshold |
| `min_faves:` | `min_faves:500` | Engagement threshold |
| `#tag` | `#osint` | Hashtag |
| `-term` | `-spam` | Exclude term |
Boolean: spaces imply AND, use uppercase `OR` for alternatives, parentheses for grouping.
#### Direct search URL
```
https://x.com/search?q=from%3A$HANDLE+since%3A2024-01-01&f=live
```
`f=live` returns chronological results instead of relevance-ranked.
## Google Dorks
```
site:x.com "$TARGET"
site:twitter.com "$TARGET"
site:x.com/i/status "$KEYWORD"
"twitter.com/$HANDLE" OR "x.com/$HANDLE"
```
Old `twitter.com` URLs are still indexed separately from `x.com`, search both.
## Deleted and Archived Content
### Wayback Machine
```
https://web.archive.org/web/*/twitter.com/$HANDLE/status/*
https://web.archive.org/web/*/x.com/$HANDLE/status/*
```
Manually browse snapshots, or use [waybacktweets](https://github.com/claromes/waybacktweets) to batch-retrieve CDX data:
```bash
pip install waybacktweets
waybacktweets $HANDLE
```
Outputs CSV/JSON with archived tweet URLs. Useful for deleted posts and suspended accounts.
### Twayback
Web tool wrapping the same Wayback CDX API with a UI:
```
https://twayback.space/
```
Note: only works if the tweet was crawled before deletion.
### Profile history
The Wayback Machine also archives profile pages: past bios, display names, profile photos, header images. Check snapshots at:
```
https://web.archive.org/web/*/twitter.com/$HANDLE
```
@@ -1,38 +0,0 @@
---
title: "Directory Discovery"
description: "Techniques and tools for discovering hidden directories and files on web servers."
tags: ["web", "enumeration", "discovery", "directory"]
publishDate: 2026-06-01
---
## FFUF
See also [FFUF](/notes/web/ffuf) for fuzzing-based directory discovery.
## Robots.txt
```bash
curl -s $url/robots.txt
```
## Sitemap.xml
```bash
curl -s $url/sitemap.xml
```
## Dirb
```bash
dirb $url
```
## Spider - Katana
A spider is a tool that crawls a website and collects information about its
structure and content. It can be used to find hidden directories, files, and
parameters.
```bash
katana -c 15 -p 15 -u $url > output
```
-33
View File
@@ -1,33 +0,0 @@
---
title: "FFUF"
description: "Reference and usage examples for ffuf, a fast web fuzzer for directories, endpoints and subdomains."
tags:
["web", "enumeration", "discovery", "subdomain", "directory", "bruteforce"]
publishDate: 2026-06-01
---
**Fuff (or ffuf)** is a fast web fuzzer written in Go, mainly used in
cybersecurity to discover hidden directories, files, API endpoints, subdomains,
vhosts and more. Its speed and flexibility make it a must-have for pentesters
and bug bounty hunters.
```bash
# Flags:
# -rate 50 -t 50 # Limit requests to 50 per second with 50 concurrent threads
# -X POST|GET|PUT # Set method
# -e .php,.asp,.bak,.db # Set the extension
# -recursion -recursion-depth 3 # Recursive fuzzing up to 3 levels deep
# -fc 404,500 # Exclude responses with status codes 404 and 500
# Examples:
ffuf -w wordlist.txt -u $url/FUZZ # Basic directory/file fuzzing using a wordlist
ffuf -w subdomains.txt -u https://FUZZ.$url # Subdomain fuzzing
ffuf -w vhosts.txt -u $url -H "Host: https://FUZZ.$url" # Virtual host fuzzing by modifying the Host header
ffuf -w wordlist.txt -u $url/page.php?FUZZ=value # GET parameter fuzzing in the query string
ffuf -w wordlist.txt -u $url/api -X POST -d 'FUZZ=value' # POST body parameter fuzzing
ffuf -w wordlist.txt -u $url/FUZZ -b 'session=abcdef' # Use a session cookie during fuzzing
ffuf -w headers.txt -u $url -H "X-Custom-Header: FUZZ" # HTTP header fuzzing
ffuf -w passwords.txt -X POST -u $url/login -d "username=admin&password=FUZZ" # Password brute-forcing for user "admin"
ffuf -w users.txt:USER -w passwords.txt:PASS -u "$url/login?username=USER&password=PASS" -mode pitchfork # Pitchfork mode: matches each line from both wordlists (USER[i], PASS[i])
ffuf -w users.txt:USER -w passwords.txt:PASS -u "$url/login?username=USER&password=PASS" -mode clusterbomb # Clusterbomb mode: tests every user with every password combination
```
@@ -1,85 +0,0 @@
---
title: "Subdomains Discovery"
description: "Methods and tools for enumerating subdomains of a target domain."
tags: ["web", "enumeration", "discovery", "subdomain"]
publishDate: 2026-06-01
---
## FFUF
See also [FFUF](/notes/web/ffuf) for fuzzing-based subdomain discovery.
## Google Dorking
Google dorks can surface subdomains indexed by Google without any active scanning.
```
site:*.$domain
site:*.$domain -www
site:*.$domain inurl:admin
site:*.$domain ext:php | ext:json | ext:xml
```
## Certificate Transparency
CT logs record every TLS certificate ever issued for a domain. Querying them is
passive and reliable.
```bash
curl -s "https://crt.sh/?q=%25.$domain&output=json" | jq '.[].name_value' | sort -u
```
Tools that aggregate CT logs:
- [crt.sh](https://crt.sh)
- [censys.io](https://search.censys.io)
## Passive DNS
Passive DNS databases store historical DNS resolutions collected from resolvers
worldwide; useful for finding subdomains that no longer resolve but once did.
```bash
# Amass (passive mode, no active scanning)
amass enum -passive -d $domain
# subfinder (uses many passive sources)
subfinder -d $domain -silent
```
## DMARC
DMARC can reveal more domains associated with a target.
Go to `dmarc.live/info/$domain`, it allows you to find domains using the
same DMARC record.
## ASN & IP Ranges
Finding the ASN of a target exposes its entire IP range, which may contain
undiscovered subdomains or related infrastructure.
```bash
# Get ASN from an IP
whois $ip | grep -i "asn\|orgname\|origin"
# Get IP ranges from ASN
whois -h whois.radb.net -- '-i origin AS12345' | grep route
```
## Favicon Hash
A unique favicon can be fingerprinted to find other domains hosted by the same
organisation, including subdomains on non-standard ports.
```bash
# Compute the MMH3 hash of the favicon
python3 -c "
import requests, mmh3, base64
r = requests.get('https://$domain/favicon.ico')
h = mmh3.hash(base64.encodebytes(r.content))
print(h)
"
```
Then search the hash on [Shodan](https://shodan.io): `http.favicon.hash:<hash>`
-636
View File
@@ -1,636 +0,0 @@
[
{
"id": 1,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "[email protected]",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "nixy",
"full_name": "anotherhadi/nixy",
"description": "Nixy simplifies and unifies the Hyprland ecosystem with a modular, easily customizable setup. It provides a structured way to manage your system configuration and dotfiles with minimal effort.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 430163,
"language": "Nix",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/nixy/languages",
"html_url": "https://git.hadi.icu/anotherhadi/nixy",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/nixy",
"link": "",
"ssh_url": "[email protected]:anotherhadi/nixy.git",
"clone_url": "https://git.hadi.icu/anotherhadi/nixy.git",
"original_url": "https://github.com/anotherhadi/nixy",
"website": "",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-03-30T17:31:04+02:00",
"updated_at": "2026-05-07T09:47:53+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "https://git.hadi.icu/repo-avatars/b232b4486d5397e743db2fe6b581d2fa4eb4d41d5bbe2c8f1dbafd6021b352b0",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-05-07T16:22:32+02:00",
"topics": [
"dotfiles",
"hyprland",
"nixos",
"nixos-configuration"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/nixy",
"gitlab": "https://gitlab.com/anotherhadi_mirror/nixy"
},
"banner_url": "https://git.hadi.icu/anotherhadi/nixy/raw/branch/main/.github/assets/banner.png"
},
{
"id": 9,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "[email protected]",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "usbguard-tui",
"full_name": "anotherhadi/usbguard-tui",
"description": "A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 425,
"language": "Go",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui/languages",
"html_url": "https://git.hadi.icu/anotherhadi/usbguard-tui",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui",
"link": "",
"ssh_url": "[email protected]:anotherhadi/usbguard-tui.git",
"clone_url": "https://git.hadi.icu/anotherhadi/usbguard-tui.git",
"original_url": "https://github.com/anotherhadi/usbguard-tui",
"website": "",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-04-30T17:40:42+02:00",
"updated_at": "2026-05-06T14:42:02+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-05-07T12:42:33+02:00",
"topics": [
"bubbletea",
"tui",
"usbguard"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/usbguard-tui",
"gitlab": "https://gitlab.com/anotherhadi_mirror/usbguard-tui"
},
"banner_url": "https://git.hadi.icu/anotherhadi/usbguard-tui/raw/branch/main/.github/assets/banner.png"
},
{
"id": 3,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "[email protected]",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "blog",
"full_name": "anotherhadi/blog",
"description": "Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 3863,
"language": "Nix",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog/languages",
"html_url": "https://git.hadi.icu/anotherhadi/blog",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog",
"link": "",
"ssh_url": "[email protected]:anotherhadi/blog.git",
"clone_url": "https://git.hadi.icu/anotherhadi/blog.git",
"original_url": "https://github.com/anotherhadi/blog",
"website": "https://hadi.icu",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-03-30T17:39:47+02:00",
"updated_at": "2026-05-04T17:03:05+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "https://git.hadi.icu/repo-avatars/527f936266a33d3ccfe22013ef2ca0f61e20a4941912b16e4b4e9dcb14bf4e30",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-05-07T17:22:32+02:00",
"topics": [
"blog",
"cybersecurity",
"portfolio"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/blog",
"gitlab": "https://gitlab.com/anotherhadi_mirror/blog"
},
"banner_url": "https://git.hadi.icu/anotherhadi/blog/raw/branch/main/.github/assets/banner.png"
},
{
"id": 6,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "[email protected]",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "iknowyou",
"full_name": "anotherhadi/iknowyou",
"description": "Self-hosted OSINT aggregation platform: Run dozens of open-source intelligence tools against a single target in parallel; all from one clean web interface.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 681,
"language": "Svelte",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/iknowyou/languages",
"html_url": "https://git.hadi.icu/anotherhadi/iknowyou",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/iknowyou",
"link": "",
"ssh_url": "[email protected]:anotherhadi/iknowyou.git",
"clone_url": "https://git.hadi.icu/anotherhadi/iknowyou.git",
"original_url": "https://github.com/anotherhadi/iknowyou",
"website": "https://iknowyou.hadi.icu",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 2,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-04-11T00:15:52+02:00",
"updated_at": "2026-04-22T00:40:03+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "https://git.hadi.icu/repo-avatars/438ce6639ec02a276f8d7b3c404e964dcf2b5269eb6da0c8fbfbb06ed8dc5a86",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-05-07T20:02:34+02:00",
"topics": [
"osint",
"osint-tool"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/iknowyou",
"gitlab": "https://gitlab.com/anotherhadi_mirror/iknowyou"
},
"banner_url": "https://git.hadi.icu/anotherhadi/iknowyou/raw/branch/main/.github/assets/banner.png"
},
{
"id": 2,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "[email protected]",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "default-creds",
"full_name": "anotherhadi/default-creds",
"description": "Default Creds is a centralized, community-driven repository of factory-set credentials. Designed for pentesters and security researchers, it helps identify weak access points during engagement phases or audit internal infrastructure before they become a security liability.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": true,
"size": 535,
"language": "Nix",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/default-creds/languages",
"html_url": "https://git.hadi.icu/anotherhadi/default-creds",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/default-creds",
"link": "",
"ssh_url": "[email protected]:anotherhadi/default-creds.git",
"clone_url": "https://git.hadi.icu/anotherhadi/default-creds.git",
"original_url": "https://github.com/anotherhadi/default-creds",
"website": "https://default-creds.hadi.icu",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-03-30T17:33:14+02:00",
"updated_at": "2026-04-03T20:44:00+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": false,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": false,
"allow_rebase": false,
"allow_rebase_explicit": false,
"allow_squash_merge": false,
"allow_fast_forward_only_merge": false,
"allow_rebase_update": false,
"allow_manual_merge": true,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "https://git.hadi.icu/repo-avatars/ca9c61caff75a89dcc15461d13c6ce6aa5289366a8aae70d12a64987b29a88c8",
"internal": false,
"mirror_interval": "8h0m0s",
"object_format_name": "sha1",
"mirror_updated": "2026-05-07T17:12:33+02:00",
"topics": [
"cybersecurity",
"cybersecurity-tools",
"default-password",
"pentesting"
],
"licenses": [
"MIT"
],
"mirrors": {
"github": "https://github.com/anotherhadi/default-creds",
"gitlab": "https://gitlab.com/anotherhadi_mirror/default-creds"
},
"banner_url": "https://git.hadi.icu/anotherhadi/default-creds/raw/branch/main/.github/assets/banner.png"
},
{
"id": 4,
"owner": {
"id": 1,
"login": "anotherhadi",
"login_name": "",
"source_id": 0,
"full_name": "Hadi",
"email": "[email protected]",
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
"html_url": "https://git.hadi.icu/anotherhadi",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2026-03-30T17:21:50+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "127.0.0.1",
"website": "https://hadi.icu",
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "anotherhadi"
},
"name": "eleakxir",
"full_name": "anotherhadi/eleakxir",
"description": "Self-hosted search engine for leaked data. An OSINT tool that lets you connect to your own private and secure server, and visualize results in a clean, modern web interface.",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": false,
"size": 664,
"language": "Svelte",
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/eleakxir/languages",
"html_url": "https://git.hadi.icu/anotherhadi/eleakxir",
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/eleakxir",
"link": "",
"ssh_url": "[email protected]:anotherhadi/eleakxir.git",
"clone_url": "https://git.hadi.icu/anotherhadi/eleakxir.git",
"original_url": "",
"website": "https://eleakxir.hadi.icu",
"stars_count": 0,
"forks_count": 0,
"watchers_count": 1,
"branch_count": 1,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "main",
"archived": false,
"created_at": "2026-03-30T17:42:13+02:00",
"updated_at": "2026-03-30T17:45:56+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"has_code": true,
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": true,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": true,
"has_pull_requests": true,
"has_projects": true,
"projects_mode": "all",
"has_releases": true,
"has_packages": true,
"has_actions": true,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": true,
"allow_rebase": true,
"allow_rebase_explicit": true,
"allow_squash_merge": true,
"allow_fast_forward_only_merge": true,
"allow_rebase_update": true,
"allow_manual_merge": false,
"autodetect_manual_merge": false,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "https://git.hadi.icu/repo-avatars/fe3f6d37cea3ee37b7dbb9fb6bfadc59fdb6b8f7a357b4245ec6d35ef6a84b3d",
"internal": false,
"mirror_interval": "",
"object_format_name": "sha1",
"mirror_updated": "0001-01-01T00:00:00Z",
"topics": [],
"licenses": [],
"mirrors": {},
"banner_url": "https://git.hadi.icu/anotherhadi/eleakxir/raw/branch/main/.github/assets/banner.png"
}
]
-164
View File
@@ -1,164 +0,0 @@
---
import Layout from "./Layout.astro";
import { Image } from "astro:assets";
import TagBadge from "../components/TagBadge.astro";
import BackToTop from "../components/BackToTop.astro";
import { ChevronLeft } from "@lucide/astro";
import { parse } from "node-html-parser";
import Author from "../components/Author.astro";
import { formatDate } from "../utils/notes";
interface Props {
title: string;
description: string;
publishDate: Date;
updatedDate?: Date;
image: any;
tags?: string[];
}
const { title, description, publishDate, updatedDate, image, tags } =
Astro.props;
const content = await Astro.slots.render("default");
const wordCount = content.split(/\s+/).length;
const readingTime = Math.ceil(wordCount / 200);
const root = parse(content);
const headers = root.querySelectorAll("h1, h2, h3");
const toc = headers.map((header) => ({
depth: parseInt(header.tagName.replace("H", "")),
text: header.innerText.trim(),
slug: header.getAttribute("id"),
}));
---
<Layout title={`${title} - Another Hadi`} description={description}>
<article class="max-w-4xl mx-auto px-4 py-20">
<BackToTop />
<div class="mb-8">
<a href="/blog" class="btn btn-ghost btn-sm">
<ChevronLeft size={18} />
Back to Blog
</a>
</div>
{
image && (
<figure class="mb-8 rounded-2xl overflow-hidden">
<Image
src={image}
alt={title}
class="w-full aspect-video object-cover"
width={1200}
height={630}
/>
</figure>
)
}
<header class="mb-8">
<h1 class="text-5xl font-bold mb-4">{title}</h1>
<p class="text-xl text-base-content/70 mb-4">{description}</p>
<div
class="flex flex-wrap items-center gap-4 text-sm text-base-content/60 mb-4"
>
<time datetime={publishDate.toISOString()}>
{formatDate(publishDate)}
</time>
<span>•</span>
<span>{readingTime} min read</span>
{
updatedDate && (
<>
<span>•</span>
<span>Updated: {formatDate(updatedDate)}</span>
</>
)
}
</div>
{
tags && tags.length > 0 && (
<div class="flex flex-wrap gap-2 mb-4">
{tags.map((tag) => (
<TagBadge tag={tag} />
))}
</div>
)
}
<Author />
</header>
<div class="divider"></div>
{
toc.length > 0 && (
<div class="collapse bg-base-200/50 rounded-xl mb-8 border border-base-300">
<input type="checkbox" />
<p class="collapse-title font-bold uppercase text-xs tracking-widest opacity-60">
Table of Contents
</p>
<div class="collapse-content text-sm">
<ul class="space-y-3">
{toc.map((item) => (
<li
class={`list-none ${item.depth === 3 ? "ml-6 text-sm" : "font-medium"}`}
>
<a
href={`#${item.slug}`}
class="hover:link transition-all flex items-center gap-2"
>
<span class="text-primary/40">{"#".repeat(item.depth)}</span>
{item.text}
</a>
</li>
))}
</ul>
</div>
</div>
)
}
<div
class="max-w-none leading-7
[&_h1]:text-4xl [&_h1]:font-bold [&_h1]:mt-8 [&_h1]:mb-4
[&_h2]:text-3xl [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-4
[&_h3]:text-2xl [&_h3]:font-bold [&_h3]:mt-8 [&_h3]:mb-4
[&_h4]:text-xl [&_h4]:font-bold [&_h4]:mt-8 [&_h4]:mb-4
[&_h5]:text-lg [&_h5]:font-bold [&_h5]:mt-8 [&_h5]:mb-4
[&_h6]:text-base [&_h6]:font-bold [&_h6]:mt-8 [&_h6]:mb-4
[&_p]:mb-4
[&_a]:underline [&_a]:link [&_a]:break-words
[&_ul]:mb-4 [&_ul]:ml-6 [&_ul]:list-disc [&_ul]:list-outside
[&_ol]:mb-4 [&_ol]:ml-6 [&_ol]:list-decimal [&_ol]:list-outside
[&_li]:mb-2
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_code]:font-mono [&_code]:bg-base-200 [&_code]:break-words [&_code]:whitespace-pre-wrap
[&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:bg-base-200
[&_pre_code]:bg-transparent [&_pre_code]:p-0
[&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4
[&_img]:rounded-lg [&_img]:my-6
[&_hr]:divider [&_hr]:border-none"
>
<slot />
</div>
<div class="divider mt-12"></div>
<div class="flex justify-center gap-2 mt-12">
<div class="flex gap-3 justify-center flex-wrap text-sm">
<a href="/blog" class="link link-hover">View All Posts</a>
<span class="text-base-content/30">•</span>
<a href="/#contact" class="link link-hover">Contact me</a>
<span class="text-base-content/30">•</span>
<a href="https://ko-fi.com/anotherhadi" class="link link-hover"
>Support me</a
>
</div>
</div>
</article>
</Layout>
+21 -98
View File
@@ -1,115 +1,38 @@
---
import "../styles/global.css";
import { ClientRouter } from "astro:transitions";
import Oneko from "../components/Oneko.astro";
import Console from "../components/Console.astro";
import Navbar from "../components/Navbar.astro";
import "../styles/global.css";
interface Props {
title?: string;
description?: string;
}
const {
title = "Another Hadi",
description = "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧",
} = Astro.props;
const anim = {
old: { name: "blurFadeOut", duration: "0.1s", easing: "ease-in-out", fillMode: "forwards" },
new: { name: "blurFadeIn", duration: "0.1s", easing: "ease-in-out", fillMode: "backwards" },
};
const pageTransition = { forwards: anim, backwards: anim };
const origin = Astro.url.origin;
const { title = "hadi's site" } = Astro.props;
---
<!doctype html>
<html lang="en" transition:animate={pageTransition} data-theme="hadi">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<meta name="description" content={title} />
<title>{title}</title>
<ClientRouter />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={origin} />
<meta property="og:image" content={`${origin}/images/og_home.png`} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={`${origin}/images/og_home.png`} />
<link
rel="alternate"
type="application/rss+xml"
title="Another Hadi RSS Feed"
href="/rss.xml"
/>
<script
is:inline
defer
src="https://umami.hadi.icu/script.js"
data-website-id="91b0c3a1-130a-4974-be47-078bc092cec8"
data-domains="hadi.icu,www.hadi.icu"></script>
</head>
<body class="min-h-screen pt-12">
<Navbar />
<body class="min-h-screen flex flex-col items-center px-4">
<div class="w-full max-w-[65ch] flex-1 flex flex-col">
<nav class="mt-8 flex items-center gap-3">
<div id="oneko-track" class="flex-1 relative h-8 pointer-events-none"></div>
<a href="/">home</a>
<a href="https://medium.com/@anotherhadi" target="_blank" rel="noopener noreferrer">blog</a>
<a href="/contact">contact</a>
</nav>
<main class="flex-1 py-10">
<slot />
</main>
</div>
<footer class="w-full max-w-[65ch] py-6 text-center text-subtext">
<p>&copy; 2026 hadi</p>
</footer>
<Oneko />
<Console />
<style is:global>
html {
scroll-behavior: smooth;
}
@keyframes pageLoadBlurFade {
0% {
opacity: 0;
filter: blur(8px);
}
100% {
opacity: 1;
filter: blur(0px);
}
}
html {
animation: pageLoadBlurFade 0.1s ease-in-out;
animation-fill-mode: both;
}
@keyframes blurFadeIn {
0% {
opacity: 0;
filter: blur(10px);
}
100% {
opacity: 1;
filter: blur(0px);
}
}
@keyframes blurFadeOut {
0% {
opacity: 1;
filter: blur(0px);
}
100% {
opacity: 0;
filter: blur(10px);
}
}
</style>
</body>
</html>
-50
View File
@@ -1,50 +0,0 @@
---
import Layout from "../layouts/Layout.astro";
import { House, FolderOpen } from "@lucide/astro";
---
<Layout
title="403 - Forbidden | Another Hadi"
description="You don't have permission to access this page."
>
<div class="min-h-screen flex items-center justify-center px-4 py-20">
<div class="text-center max-w-2xl mx-auto">
<!-- Large 403 Number -->
<h1 class="text-9xl font-bold text-primary mb-4 animate-pulse">403</h1>
<!-- Error Title -->
<h2 class="text-4xl font-bold mb-4">🥀 Forbidden</h2>
<!-- Error Description -->
<p class="text-xl mb-8 text-base-content/70">
Oops! You don't have permission to access this page. Let's get you back
on track.
</p>
<!-- Divider -->
<div class="divider"></div>
<!-- Action Buttons -->
<div class="flex gap-4 justify-center flex-wrap mt-8">
<a href="/" class="btn btn-primary gap-2">
<House class="size-5" />
Go Home
</a>
</div>
<!-- Helpful Links -->
<div class="flex justify-center gap-2 mt-12">
<p class="text-sm text-base-content/60 mb-4">
You might be looking for:
</p>
<div class="flex gap-3 justify-center flex-wrap text-sm">
<a href="/blog" class="link link-hover">Blog</a>
<span class="text-base-content/30">•</span>
<a href="/projects" class="link link-hover">Projects</a>
<span class="text-base-content/30">•</span>
<a href="/notes" class="link link-hover">Infosec Notes</a>
</div>
</div>
</div>
</div>
</Layout>
-50
View File
@@ -1,50 +0,0 @@
---
import Layout from "../layouts/Layout.astro";
import { House, FolderOpen } from "@lucide/astro";
---
<Layout
title="404 - Page Not Found | Another Hadi"
description="The page you're looking for doesn't exist."
>
<div class="min-h-screen flex items-center justify-center px-4 py-20">
<div class="text-center max-w-2xl mx-auto">
<!-- Large 404 Number -->
<h1 class="text-9xl font-bold text-primary mb-4 animate-pulse">404</h1>
<!-- Error Title -->
<h2 class="text-4xl font-bold mb-4">🥀 Page Not Found</h2>
<!-- Error Description -->
<p class="text-xl mb-8 text-base-content/70">
Oops! The page you're looking for doesn't exist or has been moved. Let's
get you back on track.
</p>
<!-- Divider -->
<div class="divider"></div>
<!-- Action Buttons -->
<div class="flex gap-4 justify-center flex-wrap mt-8">
<a href="/" class="btn btn-primary gap-2">
<House class="size-5" />
Go Home
</a>
</div>
<!-- Helpful Links -->
<div class="flex justify-center gap-2 mt-12">
<p class="text-sm text-base-content/60 mb-4">
You might be looking for:
</p>
<div class="flex gap-3 justify-center flex-wrap text-sm">
<a href="/blog" class="link link-hover">Blog</a>
<span class="text-base-content/30">•</span>
<a href="/projects" class="link link-hover">Projects</a>
<span class="text-base-content/30">•</span>
<a href="/notes" class="link link-hover">Infosec Notes</a>
</div>
</div>
</div>
</div>
</Layout>
-81
View File
@@ -1,81 +0,0 @@
---
import Layout from "../layouts/Layout.astro";
import { House, ArrowLeft } from "@lucide/astro";
interface Props {
error?: unknown;
}
const { error } = Astro.props;
// Sanitize error for display - NEVER expose sensitive server details in production
const displayMessage =
"An unexpected error occurred while processing your request.";
// Log full error server-side for debugging (only visible in server logs)
if (error instanceof Error) {
console.error("500 Server Error:", {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
});
}
---
<Layout
title="500 - Server Error | Another Hadi"
description="An error occurred while processing your request."
>
<div class="min-h-screen flex items-center justify-center px-4 py-20">
<div class="text-center max-w-2xl mx-auto">
<!-- Large 500 Number -->
<h1 class="text-9xl font-bold text-error mb-4 animate-pulse">500</h1>
<!-- Error Title -->
<h2 class="text-4xl font-bold mb-4">Server Error</h2>
<!-- Error Description -->
<p class="text-xl mb-8 text-base-content/70">
{displayMessage}
<br />
Please try again later or contact me if the problem persists.
</p>
<!-- Divider -->
<div class="divider"></div>
<!-- Action Buttons -->
<div class="flex gap-4 justify-center flex-wrap mt-8">
<a href="/" class="btn btn-primary gap-2">
<House class="size-5" />
Go Home
</a>
<button onclick="history.back()" class="btn btn-outline gap-2">
<ArrowLeft class="size-5" />
Go Back
</button>
</div>
<!-- Helpful Links -->
<div class="mt-8">
<p class="text-sm text-base-content/60 mb-4">Need help?</p>
<div class="flex gap-3 justify-center flex-wrap text-sm">
<a href="/#contact" class="link link-hover">Contact</a>
<span class="text-base-content/30">•</span>
<a
href="https://github.com/anotherhadi/blog/issues"
target="_blank"
rel="noopener noreferrer"
class="link link-hover">Report Issue</a
>
<span class="text-base-content/30">•</span>
<a href="/blog" class="link link-hover">Blog</a>
<span class="text-base-content/30">•</span>
<a href="/projects" class="link link-hover">Projects</a>
<span class="text-base-content/30">•</span>
<a href="/notes" class="link link-hover">Infosec Notes</a>
</div>
</div>
</div>
</div>
</Layout>
-81
View File
@@ -1,81 +0,0 @@
---
import Layout from "../layouts/Layout.astro";
import { House, ArrowLeft } from "@lucide/astro";
interface Props {
error?: unknown;
}
const { error } = Astro.props;
// Sanitize error for display - NEVER expose sensitive server details in production
const displayMessage =
"An unexpected error occurred while processing your request.";
// Log full error server-side for debugging (only visible in server logs)
if (error instanceof Error) {
console.error("503 Service Unavaiable:", {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
});
}
---
<Layout
title="503 - Service Unavaiable | Another Hadi"
description="An error occurred while processing your request."
>
<div class="min-h-screen flex items-center justify-center px-4 py-20">
<div class="text-center max-w-2xl mx-auto">
<!-- Large 503 Number -->
<h1 class="text-9xl font-bold text-error mb-4 animate-pulse">503</h1>
<!-- Error Title -->
<h2 class="text-4xl font-bold mb-4">Service Unavailable</h2>
<!-- Error Description -->
<p class="text-xl mb-8 text-base-content/70">
{displayMessage}
<br />
Please try again later or contact me if the problem persists.
</p>
<!-- Divider -->
<div class="divider"></div>
<!-- Action Buttons -->
<div class="flex gap-4 justify-center flex-wrap mt-8">
<a href="/" class="btn btn-primary gap-2">
<House class="size-5" />
Go Home
</a>
<button onclick="history.back()" class="btn btn-outline gap-2">
<ArrowLeft class="size-5" />
Go Back
</button>
</div>
<!-- Helpful Links -->
<div class="mt-8">
<p class="text-sm text-base-content/60 mb-4">Need help?</p>
<div class="flex gap-3 justify-center flex-wrap text-sm">
<a href="/#contact" class="link link-hover">Contact</a>
<span class="text-base-content/30">•</span>
<a
href="https://github.com/anotherhadi/blog/issues"
target="_blank"
rel="noopener noreferrer"
class="link link-hover">Report Issue</a
>
<span class="text-base-content/30">•</span>
<a href="/blog" class="link link-hover">Blog</a>
<span class="text-base-content/30">•</span>
<a href="/projects" class="link link-hover">Projects</a>
<span class="text-base-content/30">•</span>
<a href="/notes" class="link link-hover">Infosec Notes</a>
</div>
</div>
</div>
</div>
</Layout>
-27
View File
@@ -1,27 +0,0 @@
---
import { getCollection, render } from "astro:content";
import BlogLayout from "../../layouts/BlogLayout.astro";
export async function getStaticPaths() {
const blogEntries = await getCollection("blog");
return blogEntries.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<BlogLayout
title={entry.data.title}
description={entry.data.description}
publishDate={entry.data.publishDate}
updatedDate={entry.data.updatedDate}
image={entry.data.image}
tags={entry.data.tags}
>
<Content />
</BlogLayout>
-59
View File
@@ -1,59 +0,0 @@
---
import Layout from "../../layouts/Layout.astro";
import { getCollection } from "astro:content";
import { ChevronLeft } from "@lucide/astro";
import BlogCard from "../../components/BlogCard.astro";
const blogPosts = await getCollection("blog");
// Sort by publish date, most recent first
const sortedPosts = blogPosts.sort(
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
);
---
<Layout
title="Blog - Another Hadi"
description="Read my latest blog posts and articles about cybersecurity, OSINT and technology."
>
<main class="max-w-6xl mx-auto px-4 py-20">
<!-- Header -->
<div class="text-center mb-16">
<h1 class="text-5xl font-bold mb-4">Blog</h1>
<p class="text-xl text-base-content/70">
Thoughts, insights, and tutorials on cybersecurity, OSINT, and
technology.
</p>
</div>
<!-- Back to Home -->
<div class="mb-8">
<a href="/" class="btn btn-ghost btn-sm">
<ChevronLeft size={18} />
Back to Home
</a>
</div>
<!-- Blog Posts Grid -->
{
sortedPosts.length === 0 ? (
<div class="text-center py-20">
<p class="text-2xl text-base-content/60">
No blog posts yet. Check back soon!
</p>
</div>
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{sortedPosts.map((post) => (
<BlogCard
displayBanner={true}
displayTags={true}
displayDate={true}
post={post}
/>
))}
</div>
)
}
</main>
</Layout>
+18
View File
@@ -0,0 +1,18 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="contact - hadi's site">
<h1 class="text-2xl font-bold mb-4">contact</h1>
<p class="leading-relaxed">
send a mail to <span class="text-accent"
>anotherhadi.clapped234 [at] passmail [dot] net</span
>
and i'll get back to you whenever i can.
</p>
<p class="leading-relaxed mt-2">
if you need secure communication, use my <a href="/anotherhadi.asc"
>gpg key</a
>.
</p>
</Layout>
+73 -27
View File
@@ -1,33 +1,79 @@
---
import Layout from "../layouts/Layout.astro";
import Hero from "../components/Hero.astro";
import Projects from "../components/Projects.astro";
import Blog from "../components/Blog.astro";
import Notes from "../components/Notes.astro";
import Contact from "../components/Contact.astro";
import { siteConfig } from "../config";
import avatar from "../../public/avatar.jpg";
---
<Layout title={`Another Hadi`} description={siteConfig.description}>
<main class="px-10">
<Hero
name={siteConfig.name}
title={siteConfig.title}
description={siteConfig.description}
avatar={avatar}
socialLinks={siteConfig.socialLinks}
gpgKey={siteConfig.gpgKey}
rssFeed={siteConfig.rssFeed}
/>
<Layout title="hadi's site">
<h1 class="text-2xl font-bold mb-4">hi, i'm hadi</h1>
<p class="mb-4 leading-relaxed">
infosec engineer passionate about linux/nixos, blockchains, osint & foss.
hacking with go, exploring open tech, and contributing whenever i can!
</p>
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Blog />
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Notes />
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Projects />
<hr class="border-base-300/30 max-w-6xl mx-auto" />
<Contact />
</main>
<h2 class="text-xl font-bold mt-8 mb-3">projects</h2>
<p class="mb-3 leading-relaxed">
i love building things myself, from scratch, just to understand how they
really work. a few i'm working on:
</p>
<ul class="list-disc pl-5 space-y-2 leading-relaxed lowercase">
<li>
<a
href="https://github.com/anotherhadi/default-creds"
target="_blank"
rel="noopener noreferrer">default-creds</a
>: Default Creds is a centralized, community-driven repository of
factory-set credentials. Designed for pentesters and security researchers,
it helps identify weak access points during engagement phases or audit
internal infrastructure before they become a security liability.
</li>
<li>
<a
href="https://github.com/anotherhadi/spilltea"
target="_blank"
rel="noopener noreferrer">spilltea</a
>: A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.
Think Burp Suite or Caido, but entirely in your terminal.
</li>
<li>
<a
href="https://github.com/anotherhadi/usbguard-tui"
target="_blank"
rel="noopener noreferrer">usbguard-tui</a
>: a TUI to manage USBGuard device policies without touching a config
file.
</li>
</ul>
<p class="mt-3 leading-relaxed">
there's plenty more on <a
href="https://github.com/anotherhadi"
target="_blank"
rel="noopener noreferrer">github</a
>, also mirrored on <a
href="https://git.hadi.icu"
target="_blank"
rel="noopener noreferrer">gitea</a
>.
</p>
<h2 class="text-xl font-bold mt-8 mb-3">blog</h2>
<p class="leading-relaxed">
i write about privacy, infosec and osint on
<a
href="https://medium.com/@anotherhadi"
target="_blank"
rel="noopener noreferrer">my blog on medium</a
> and i also keep a pile of cybersecurity cheatsheets, in
<a
href="https://github.com/anotherhadi/sec-notes"
target="_blank"
rel="noopener noreferrer">sec-notes</a
>.
</p>
<p class="mt-8 leading-relaxed text-subtext">
if you want to support what i do, there's <a
href="https://ko-fi.com/anotherhadi"
target="_blank"
rel="noopener noreferrer">ko-fi</a
>.
</p>
</Layout>
-301
View File
@@ -1,301 +0,0 @@
---
import { getCollection, render } from "astro:content";
import Layout from "../../layouts/Layout.astro";
import { List, PanelRight } from "@lucide/astro";
import NoteTOC from "../../components/NoteTOC.astro";
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
import NoteVars from "../../components/NoteVars.svelte";
import { getCategory, extractLinks, extractExternalLinks } from "../../utils/notes";
export async function getStaticPaths() {
const notes = await getCollection("notes");
return notes.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content, headings: astroHeadings } = await render(entry);
const allNotes = await getCollection("notes");
const sortedNotes = allNotes.sort((a, b) =>
a.data.title.localeCompare(b.data.title),
);
const categories = [...new Set(allNotes.map(getCategory))].sort();
const allLinks = Object.fromEntries(
allNotes.map((n) => [n.id, extractLinks(n.body ?? "")]),
);
const forwardLinks = (allLinks[entry.id] ?? [])
.map((id) => allNotes.find((n) => n.id === id))
.filter(Boolean) as typeof allNotes;
const backlinks = allNotes.filter(
(n) => n.id !== entry.id && (allLinks[n.id] ?? []).includes(entry.id),
);
const graphNodes = [
{ id: entry.id, title: entry.data.title, current: true },
...forwardLinks.map((n) => ({
id: n.id,
title: n.data.title,
current: false,
})),
...backlinks
.filter((n) => !forwardLinks.some((f) => f.id === n.id))
.map((n) => ({ id: n.id, title: n.data.title, current: false })),
];
const graphEdges = [
...forwardLinks.map((n) => ({ from: entry.id, to: n.id })),
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
];
const noteVars = [
...new Set(
Array.from(
(entry.body ?? "").matchAll(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g),
(m) => m[1],
),
),
];
const headings = astroHeadings.map((h) => ({ depth: h.depth, text: h.text, id: h.slug }));
const externalLinks = extractExternalLinks(entry.body ?? "");
---
<style>
@media (min-width: 768px) {
.drawer.md\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
}
@media (min-width: 1280px) {
.drawer.xl\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
}
</style>
<Layout
title={`${entry.data.title} - Infosec Notes`}
description={entry.data.description}
>
<main class="max-w-screen-2xl mx-auto">
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex min-h-[calc(100vh-3rem)] min-w-0">
<div class="drawer drawer-end xl:drawer-open w-full" id="right-drawer">
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col min-w-0">
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
<div class="max-w-3xl mx-auto lg:mx-0">
<div class="flex items-center justify-between mb-6">
<div
class="breadcrumbs text-xs font-mono text-base-content/35 p-0"
>
<ul>
<li>
<a href="/notes" class="hover:text-base-content/70"
>notes</a
>
</li>
<li>
{
entry.id.includes("/") ? (
<a
href={`/notes/${getCategory(entry)}`}
class="hover:text-base-content/70"
>
{getCategory(entry)}
</a>
) : (
getCategory(entry)
)
}
</li>
</ul>
</div>
<div class="flex items-center gap-2">
<label
for="nav-drawer"
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
<List size={11} />
nav
</label>
<NoteVars client:load vars={noteVars} />
<label
for="graph-drawer"
id="graph-toggle"
class="btn btn-ghost btn-xs xl:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
title="Toggle graph"
>
<PanelRight size={11} />
graph
</label>
</div>
</div>
<header class="mb-8">
<h1
class="text-4xl sm:text-5xl font-bold tracking-tight mb-3"
>
{entry.data.title}
</h1>
<p class="text-base-content/50 mb-4">
{entry.data.description}
</p>
{
entry.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mb-4">
{entry.data.tags.map((tag) => (
<a
href={`/notes?tag=${tag}`}
class="badge badge-ghost badge-xs font-mono text-base-content/30 hover:text-primary/70 transition-colors"
>
{tag}
</a>
))}
</div>
)
}
</header>
<NoteTOC headings={headings} />
<div
class="note-content text-sm leading-relaxed text-base-content/80
[&_h2]:text-lg [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-base-content [&_h2]:tracking-tight [&_h2]:pb-1.5 [&_h2]:border-b [&_h2]:border-base-300/30
[&_h3]:text-base [&_h3]:font-semibold [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-base-content/90
[&_h4]:text-sm [&_h4]:font-semibold [&_h4]:mt-4 [&_h4]:mb-2 [&_h4]:text-base-content/80
[&_p]:mb-4 [&_p]:leading-relaxed
[&_a]:text-primary/80 [&_a]:underline [&_a]:underline-offset-2 [&_a]:hover:text-primary [&_a]:transition-colors
[&_ul]:mb-4 [&_ul]:ml-5 [&_ul]:list-none [&_ul]:space-y-1
[&_ul_li]:before:content-['–'] [&_ul_li]:before:text-base-content/25 [&_ul_li]:before:mr-2 [&_ul_li]:before:font-mono
[&_ol]:mb-4 [&_ol]:ml-5 [&_ol]:list-decimal [&_ol]:space-y-1
[&_li]:text-base-content/75
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded-field [&_code]:font-mono [&_code]:text-xs [&_code]:bg-base-200 [&_code]:text-primary/80 [&_code]:border [&_code]:border-base-300/50
[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:rounded-box [&_pre]:bg-base-200/60 [&_pre]:border [&_pre]:border-base-300/50 [&_pre]:text-xs
[&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0 [&_pre_code]:text-base-content/80
[&_blockquote]:border-l-2 [&_blockquote]:border-primary/25 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4 [&_blockquote]:text-base-content/50
[&_table]:w-full [&_table]:mb-6 [&_table]:text-xs [&_table]:border-collapse
[&_th]:text-left [&_th]:px-3 [&_th]:py-2 [&_th]:border [&_th]:border-base-300/50 [&_th]:bg-base-200/60 [&_th]:font-mono [&_th]:text-[10px] [&_th]:uppercase [&_th]:tracking-widest [&_th]:text-base-content/50
[&_td]:px-3 [&_td]:py-2 [&_td]:border [&_td]:border-base-300/40 [&_td]:font-mono [&_td]:text-xs [&_td]:text-base-content/70
[&_tr:nth-child(even)_td]:bg-base-200/20
[&_hr]:border-t [&_hr]:border-base-300/30 [&_hr]:my-8"
>
<Content />
</div>
<div
class="border-t border-base-300/30 mt-12 pt-6 flex items-center justify-between font-mono text-[10px] text-base-content/25"
>
<a
href="/notes"
class="hover:text-base-content/50 transition-colors"
>
← all notes
</a>
<a
href="/"
class="hover:text-base-content/50 transition-colors"
>
~/hadi
</a>
</div>
</div>
</main>
</div>
<div class="drawer-side z-[60]">
<label
for="graph-drawer"
aria-label="close sidebar"
class="drawer-overlay"></label>
<NoteGraphSidebar
entry={entry}
graphNodes={graphNodes}
graphEdges={graphEdges}
forwardLinks={forwardLinks}
backlinks={backlinks}
externalLinks={externalLinks}
/>
</div>
</div>
</div>
<div class="drawer-side z-[70]">
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<NoteNavSidebar
notes={sortedNotes}
currentEntry={entry}
categories={categories}
/>
</div>
</div>
</main>
</Layout>
<script>
function injectHeadingAnchors() {
if (!document.getElementById("heading-anchor-styles")) {
const s = document.createElement("style");
s.id = "heading-anchor-styles";
s.textContent = `
.note-content h2:not(.link-card h2),
.note-content h3:not(.link-card h3),
.note-content h4:not(.link-card h4) {
display: flex !important;
align-items: center;
flex-wrap: wrap;
gap: 0;
}
.heading-anchor {
display: inline-flex;
align-items: center;
flex-shrink: 0;
margin-left: 0.4em;
color: oklch(38% 0 0);
opacity: 0;
transition: opacity 120ms, color 120ms;
text-decoration: none;
}
.note-content h2:hover .heading-anchor,
.note-content h3:hover .heading-anchor,
.note-content h4:hover .heading-anchor { opacity: 1; }
.heading-anchor:hover, .heading-anchor.copied { color: oklch(71% 0.0863 296.59); opacity: 1; }
`;
document.head.appendChild(s);
}
document
.querySelectorAll(".note-content h2, .note-content h3, .note-content h4")
.forEach((heading) => {
if (!heading.id || heading.querySelector(".heading-anchor")) return;
const anchor = document.createElement("a");
anchor.href = `#${heading.id}`;
anchor.className = "heading-anchor";
anchor.setAttribute("aria-label", "Copy link to section");
anchor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
anchor.addEventListener("click", (e) => {
e.preventDefault();
const url = `${location.origin}${location.pathname}#${heading.id}`;
navigator.clipboard.writeText(url).then(() => {
anchor.classList.add("copied");
setTimeout(() => anchor.classList.remove("copied"), 1800);
});
history.pushState(null, "", `#${heading.id}`);
});
heading.appendChild(anchor);
});
}
document.addEventListener("astro:page-load", () => {
injectHeadingAnchors();
});
</script>
-176
View File
@@ -1,176 +0,0 @@
---
import { getCollection } from "astro:content";
import Layout from "../../layouts/Layout.astro";
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
import { getCategory } from "../../utils/notes";
import { List } from "@lucide/astro";
export async function getStaticPaths() {
const notes = await getCollection("notes");
const folderCategories = [
...new Set(
notes.filter((n) => n.id.includes("/")).map((n) => getCategory(n)),
),
];
return folderCategories.map((category) => {
const allNotes = notes.sort((a, b) =>
a.data.title.localeCompare(b.data.title),
);
const categories = [...new Set(notes.map(getCategory))].sort();
return {
params: { category },
props: {
category,
categoryNotes: notes
.filter((n) => getCategory(n) === category)
.sort((a, b) => a.data.title.localeCompare(b.data.title)),
allNotes,
categories,
},
};
});
}
const { category, categoryNotes, allNotes, categories } = Astro.props;
if (!categoryNotes) {
return new Response(null, { status: 404, statusText: "Not found" });
}
---
<style>
@media (min-width: 768px) {
.drawer.md\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
}
</style>
<Layout
title={`${category} - Infosec Notes`}
description={`Notes on ${category}.`}
>
<main class="max-w-screen-2xl mx-auto">
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col min-w-0">
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
<div class="max-w-3xl mx-auto lg:mx-0">
<div class="flex items-center justify-between mb-10">
<div
class="breadcrumbs text-xs font-mono text-base-content/35 p-0"
>
<ul>
<li>
<a href="/notes" class="hover:text-base-content/70">notes</a
>
</li>
<li class="text-base-content/60">{category}</li>
</ul>
</div>
<label
for="nav-drawer"
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
<List size={11} />
nav
</label>
</div>
<div class="mb-10">
<div class="flex items-baseline gap-3 mb-1">
<h1 class="text-4xl sm:text-5xl font-bold">
<span class="text-primary/40 font-mono mr-1">/</span>{
category
}
</h1>
</div>
<p class="font-mono text-xs text-base-content/25 ml-8">
{categoryNotes.length} note{
categoryNotes.length !== 1 ? "s" : ""
}
</p>
</div>
<div class="border-t border-base-300/40 mb-1"></div>
<ul class="divide-y divide-base-300/20">
{
categoryNotes.map((note) => (
<li>
<a
href={`/notes/${note.id}`}
class="group flex items-center gap-4 py-3 hover:bg-base-200/30 px-2 -mx-2 transition-colors"
>
<div class="flex-1 min-w-0">
<div class="flex flex-col mb-0.5">
<span class="font-semibold text-sm group-hover:text-primary transition-colors">
{note.data.title}
</span>
{note.data.description && (
<span class="text-xs text-base-content/35 truncate">
{note.data.description}
</span>
)}
</div>
{note.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-1">
{note.data.tags.map((tag) => (
<span class="badge badge-ghost badge-xs font-mono text-base-content/30">
{tag}
</span>
))}
</div>
)}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-base-content/20 group-hover:text-primary/50 shrink-0 transition-colors"
>
<path d="m9 18 6-6-6-6" />
</svg>
</a>
</li>
))
}
</ul>
<div
class="border-t border-base-300/30 mt-12 pt-6 flex items-center justify-between font-mono text-[10px] text-base-content/25"
>
<a
href="/notes"
class="hover:text-base-content/50 transition-colors"
>
← all notes
</a>
<a href="/" class="hover:text-base-content/50 transition-colors">
~/hadi
</a>
</div>
</div>
</main>
</div>
<div class="drawer-side z-[70]">
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<NoteNavSidebar
notes={allNotes}
currentCategory={category}
categories={categories}
/>
</div>
</div>
</main>
</Layout>
-44
View File
@@ -1,44 +0,0 @@
---
import Layout from "../../layouts/Layout.astro";
import { getCollection } from "astro:content";
import { getCategory } from "../../utils/notes";
import NotesSearch from "../../components/NotesSearch.svelte";
const notes = await getCollection("notes");
const sortedNotes = notes.sort(
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
);
const searchNotes = sortedNotes.map((n) => ({
id: n.id,
title: n.data.title,
description: n.data.description,
tags: n.data.tags,
category: getCategory(n),
searchText: [n.data.title, n.data.description, n.body ?? ""]
.join(" ")
.toLowerCase(),
}));
---
<Layout
title="Infosec Notes - Another Hadi"
description="Cheatsheets on cybersecurity tools and techniques."
>
<main class="max-w-4xl mx-auto px-4 py-16 sm:py-20">
<div class="text-center mb-12">
<h1 class="text-4xl sm:text-5xl font-bold mb-4">Infosec Notes</h1>
<p class="text-xl text-base-content/70">
Cheat sheets on cybersecurity tools and techniques.
</p>
</div>
<div
class="text-xs font-mono text-base-content/25 border border-base-300/30 rounded-box px-4 py-3 mb-10 max-w-xl mx-auto text-center leading-relaxed"
>
All content is intended for educational purposes, CTF challenges, and
authorized penetration testing only. Do not use any of this against
systems you do not own or have explicit permission to test.
</div>
<NotesSearch client:load notes={searchNotes} />
</main>
</Layout>
-45
View File
@@ -1,45 +0,0 @@
---
import Layout from "../../layouts/Layout.astro";
import GiteaProjectCard from "../../components/GiteaProjectCard.astro";
import { ChevronLeft } from "@lucide/astro";
import repos from "../../data/repos.json";
---
<Layout
title="Projects - Another Hadi"
description="Explore my latest projects and work"
>
<main class="max-w-6xl mx-auto px-4 py-20">
<div class="text-center mb-16">
<h1 class="text-5xl font-bold mb-4">Projects</h1>
<p class="text-xl text-base-content/70">
I enjoy the challenge of reimagining existing programs & scripts in my
own unique way.
</p>
</div>
<div class="mb-8">
<a href="/" class="btn btn-ghost btn-sm">
<ChevronLeft size={18} />
Back to Home
</a>
</div>
{
repos.length === 0 ? (
<div class="text-center py-20">
<p class="text-2xl text-base-content/60">
No projects found. Check back soon!
</p>
</div>
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{repos.map((repo) => (
<GiteaProjectCard displayBanner={true} repo={repo} />
))}
</div>
)
}
</main>
</Layout>
-32
View File
@@ -1,32 +0,0 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import type { APIContext } from "astro";
export async function GET(context: APIContext) {
const blog = await getCollection("blog");
const blogItems = blog.map((post) => ({
title: post.data.title,
pubDate: post.data.publishDate,
description: post.data.description,
link: `/blog/${post.id}/`,
enclosure: {
url: new URL(post.data.image.src, context.site).toString(),
length: 0,
type: "image/png",
},
}));
const allItems = [...blogItems].sort(
(a, b) => b.pubDate.getTime() - a.pubDate.getTime(),
);
return rss({
title: "Another Hadi - Blog posts",
description:
"Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
site: context.site!,
items: allItems,
customData: `<language>en</language>`,
});
}
+17 -96
View File
@@ -1,106 +1,27 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "hadi";
default: true;
prefersdark: false;
color-scheme: dark;
--color-base-100: oklch(0% 0 0);
--color-base-200: oklch(19% 0 0);
--color-base-300: oklch(22% 0 0);
--color-base-content: oklch(87.609% 0 0);
--color-primary: oklch(71% 0.0863 296.59);
--color-primary-content: oklch(100% 0 0);
--color-secondary: oklch(35% 0 0);
--color-secondary-content: oklch(100% 0 0);
--color-accent: oklch(35% 0 0);
--color-accent-content: oklch(100% 0 0);
--color-neutral: oklch(35% 0 0);
--color-neutral-content: oklch(100% 0 0);
--color-info: oklch(45.201% 0.313 264.052);
--color-info-content: oklch(89.04% 0.062 264.052);
--color-success: oklch(51.975% 0.176 142.495);
--color-success-content: oklch(90.395% 0.035 142.495);
--color-warning: oklch(96.798% 0.211 109.769);
--color-warning-content: oklch(19.359% 0.042 109.769);
--color-error: oklch(62.795% 0.257 29.233);
--color-error-content: oklch(12.559% 0.051 29.233);
--radius-selector: 0.25rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 0;
--noise: 0;
@theme {
--color-bg: #08080a;
--color-fg: #ffffff;
--color-subtext: #8a8a8a;
--color-accent: #9e97f8;
}
.drawer-side > aside > astro-island {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
* {
font-family: -apple-system, system-ui, "Helvetica", "Arial", sans-serif;
}
.btn:not(.btn-circle):not(.btn-square) {
@apply rounded-lg;
}
@layer components {
.link-card {
display: flex;
align-items: center;
padding: 0 0.875rem;
border-radius: var(--radius-box);
border: 1px solid oklch(24% 0 0);
background: transparent;
color: var(--color-base-content);
text-decoration: none !important;
transition:
background 0.15s ease,
border-color 0.15s ease;
margin-block: 0.25rem;
}
.link-card::after {
content: "↗";
margin-left: auto;
padding-left: 0.75rem;
opacity: 0;
color: var(--color-primary);
font-size: 0.75rem;
transition:
opacity 0.15s ease,
transform 0.15s ease;
transform: translate(-4px, 4px);
flex-shrink: 0;
}
.link-card:hover {
background: var(--color-base-200);
border-color: var(--color-primary);
}
.link-card:hover::after {
opacity: 1;
transform: translate(0, 0);
}
.link-card span {
display: flex;
flex-direction: column;
}
.link-card h4 {
font-size: 0.85rem;
font-weight: 600;
body {
margin: 0;
line-height: 1;
transition: color 0.15s ease;
background-color: var(--color-bg);
color: var(--color-fg);
}
.link-card:hover h4 {
color: var(--color-primary);
}
.link-card p {
font-size: 0.75rem;
margin: 0;
line-height: 1;
color: oklch(52% 0 0);
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
-80
View File
@@ -1,80 +0,0 @@
export function getCategory(n: {
id: string;
data: { category?: string };
}): string {
if (n.data.category) return n.data.category;
const parts = n.id.split("/");
return parts.length > 1 ? parts[0] : "General";
}
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
.trim()
.replace(/ +/g, "-");
}
export function extractExternalLinks(body: string): { url: string; label: string }[] {
const seen = new Set<string>();
const links: { url: string; label: string }[] = [];
// Markdown: [label](https://...)
const mdRe = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
let m;
while ((m = mdRe.exec(body)) !== null) {
if (!seen.has(m[2])) {
seen.add(m[2]);
links.push({ url: m[2], label: m[1] });
}
}
// HTML: <a href="https://...">...</a> — use h4 content as label if present, else href host
const htmlRe = /<a\s[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
while ((m = htmlRe.exec(body)) !== null) {
const url = m[1];
if (seen.has(url)) continue;
seen.add(url);
const h4 = m[2].match(/<h4[^>]*>([\s\S]*?)<\/h4>/);
const label = h4 ? h4[1].trim() : new URL(url).hostname;
links.push({ url, label });
}
return links;
}
export function extractLinks(body: string): string[] {
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
const ids: string[] = [];
let m;
while ((m = re.exec(body)) !== null) ids.push(m[1]);
return [...new Set(ids)];
}
export function formatDate(date: Date): string {
return date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}
export function extractHeadings(
body: string,
): { depth: number; text: string; id: string }[] {
const headings: { depth: number; text: string; id: string }[] = [];
const re = /^(#{2,4}) (.+)$/gm;
let m;
while ((m = re.exec(body)) !== null) {
const raw = m[2]
.trim()
.replace(/`[^`]*`/g, "")
.replace(/\*\*(.*?)\*\*/g, "$1")
.replace(/(?<!\p{L}\p{N})__(.*?)__(?!\p{L}\p{N})/gu, "$1")
.replace(/\*(.*?)\*/g, "$1")
.replace(/(?<!\p{L}\p{N})_(.*?)_(?!\p{L}\p{N})/gu, "$1")
.replace(/[*]/g, "");
headings.push({ depth: m[1].length, text: raw, id: slugify(raw) });
}
return headings;
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist", "result"]
"exclude": ["dist"]
}