Step 294. CTF Debrief Block A + 50 Write-ups + Blog Tidy-Up — The Moment Records Become a Career
Level 3 — Real-World CTF & Advanced Offensive Skills | Difficulty ★★★☆☆ | Estimated time: 2 days (half a day of debriefing + a half-day timebox of blog tidy-up)
Prerequisites: Step 292’s 40 write-ups accumulated, Step 293’s competition #8 (the role-rotation experiment) complete.
- What you need: the blog holding your write-ups so far (static blog or team repository, either works), competition #8’s competition log, a Python environment, and a half-day for tidying. The competition debrief scenes in this chapter are output examples; the blog audit script’s results are something you verify hands-on in your own folder.
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Only use problems that are retired or from finished competitions in your write-ups — publishing solutions to active machines violates platform rules.
- This chapter is tidying — no new problems to solve; we polish the accumulated records into "an asset others can read."
Fifty write-ups reads in the CTF community as the baseline of "a consistently active player." Luck can produce one or two well-written pieces, but only time and routine can produce an accumulation of 50. So this number reads as the same sentence to recruiters and the community alike — "this person doesn’t stop."
The problem is that a blog with 50 pieces piled up often fails to consider its readers. Tags run wild, the best pieces are buried, and a visitor can’t figure out "what this person is good at" within 30 seconds. Today, after completing 50 with the competition #8 debrief, we tidy the blog into portfolio v1. A first completion before Level 4’s career applications arrive.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Dig competition #8’s unsolved problems to the bottom with the debrief block A routine and reach 50 write-ups
- Automatically audit the blog’s category/tag consistency and broken links with a script
- Select 5 representative pieces from a hiring/networking standpoint and place them on an intro page
- Organize the About page with your curriculum journey, competition record, and tech stack
- Limit the tidy-up to a half-day timebox to prevent content production from stalling
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 (audit script), Markdown (write-ups, About), any blog platform |
| Today’s command | python blog_audit.py <posts folder> — the tidy-up auditor you build yourself |
| Concepts needed | The debrief block A routine (Step 280), representative-piece selection criteria, the timebox |
| Today’s deliverable | 50 write-ups accumulated + a tidied blog (5 representative pieces + About + index) |
2-1. The Weight of 50 — What the Number Says
A write-up’s value lies not in the count but in the accumulation’s power of proof. When a recruiter opens an applicant’s blog on a hiring screen, they look at three things — consistency (are intervals regular), growth (are recent pieces better than old ones), communicability (is it written reproducibly). Fifty is the minimum volume that shows all three at once.
Same in networking. When a player you met at a competition takes your blog address and drops in, a list of 50 pieces organized by field is itself a business card. Leaving the memory "for web, ask this person" — that’s a portfolio’s first job.
2-2. The Reader’s 30 Seconds — Representative Pieces and Series Bundling
A visitor gives your blog 30 seconds. What must reach them in that time is not "the latest piece" but "the best pieces." So the tidy-up’s first job is selecting representative pieces.
The criterion for representative pieces is not like counts. Look at these four.
| Criterion | Question to check |
|---|---|
| Difficulty | Not an easy problem — one that pushed the you of that time |
| Narrative | Are trial-and-error and discarded hypotheses visible (success-path-only is rejected) |
| Reproducibility | Can a reader follow along with just the commands and output |
| Field balance | The 5 pieces aren’t clumped into one field |
Next is series bundling. The device that makes 50 scattered pieces get read is not a list but narrative — bundle them like "growth record from competition #1 to #8" or "the 10-problem heap drill series," and visitors click through to the next piece.
2-3. What Gets Tidied — What Machines Can Find and What They Can’t
Blog tidying splits into two layers. Defects machines find — missing categories, tags outside the allowed list (using web then one day mixing in webb, Web), broken images and internal links. The script catches these. Defects humans find — the quality of representative pieces, the tone of the About page, the flow of series. Your eyes catch these.
Today’s script handles the first layer. Instead of opening all 50 by hand, scan the whole posts folder and extract defect coordinates — the blog edition of "killing repetition with automation" you learned in CTF.
2-4. The Timebox — Don’t Let Tidying Eat Production
Moving the source’s warning over verbatim — the trap of getting absorbed in blog decorating and halting content production. Start changing the theme and picking fonts and half a day vanishes in a blink, while next competition’s prep slips.
So today’s tidy-up is a half-day timebox. Turn on a clock, and every task not on the list (theme changes, logo design, visitor counters) is forbidden. Tidying is not renovation but organizing — putting things away in their places, not building a new house.
3. Follow Along
3-1. Filling 50 with the Block A Debrief
Competition #8 was the role-rotation experiment, so there’s more debrief material than usual — problems you got stuck on in non-main fields are exactly the debrief targets. By Step 280’s block A routine as-is, pick three "almost had it" problems and fill the debrief tables.
Output example (debrief table — competition #8, a problem from the non-main field taken on rotation):
Problem: rsa-fault (crypto, 350 pts)
Where I reached: discovered N was shared across two problems, but stopped, not knowing how to use it
The answer's next step: factor via the common factor gcd(N1, N2) (common-factor attack)
What was needed to know that: the existence of the "RSA key pair reusing the same p" pattern
Playbook reflection: crypto/common-factor.md — one line of gcd and done. Marked for immediate use at the next competition
How to read it: a rotation competition’s debrief adds one line — "how many minutes would it have taken the main-field owner?" If it’s a problem you requested consulting on, you know that answer. That difference is exactly the distance between your secondary field and the main one, and the basis for drill volume.
Move the three debriefs into write-ups and the accumulation reaches 50. The 48-hour rough-draft rule (Step 282) still holds — two days after a competition ends, the details of trial-and-error evaporate.
3-2. The Tidy-Up Audit Script — Don’t Open 50 by Hand
Now the machine layer of blog tidying. Build a script that scans the posts folder and extracts category distribution, tag consistency, and broken links in one pass. Save the code below as blog_audit.py.
# blog_audit.py — write-up blog tidy-up auditor
# usage: python blog_audit.py <blog posts folder>
import re
import sys
from pathlib import Path
ALLOWED_TAGS = {"web", "pwn", "rev", "crypto", "forensics", "misc", "ctf"}
def parse_post(path: Path):
text = path.read_text(encoding="utf-8")
fm = re.match(r"^---\n(.*?)\n---\n", text, re.S)
tags, category = [], None
if fm:
m = re.search(r"^tags:\s*\[(.*?)\]", fm.group(1), re.M)
if m:
tags = [t.strip().strip('"').strip("'") for t in m.group(1).split(",") if t.strip()]
m = re.search(r"^category:\s*(.+)$", fm.group(1), re.M)
if m:
category = m.group(1).strip()
targets = re.findall(r"!?\[[^\]]*\]\((?!https?://)([^)]+)\)", text)
return category, tags, targets
def main():
root = Path(sys.argv[1])
posts = sorted(root.glob("*.md"))
cats, bad_tags, broken = {}, [], []
for p in posts:
category, tags, targets = parse_post(p)
cats[category or "(none)"] = cats.get(category or "(none)", 0) + 1
for t in tags:
if t.lower() not in ALLOWED_TAGS:
bad_tags.append((p.name, t))
for tgt in targets:
clean = tgt.split("#")[0]
if clean and not (p.parent / clean).exists():
broken.append((p.name, tgt))
print(f"Accumulated write-ups: {len(posts)}")
print("\n[Category distribution]")
for c, n in sorted(cats.items(), key=lambda x: -x[1]):
print(f" {c}: {n}")
print(f"\n[Tags outside the allowed list] {len(bad_tags)}")
for name, t in bad_tags:
print(f" {name} -> tag '{t}'")
print(f"\n[Broken internal links/images] {len(broken)}")
for name, tgt in broken:
print(f" {name} -> {tgt}")
if __name__ == "__main__":
main()
Here’s the actual run — measured against a sample folder of 52 write-ups (with defects deliberately planted):
$ python blog_audit.py blog294/posts
Accumulated write-ups: 52
[Category distribution]
web: 15
pwn: 12
rev: 8
crypto: 8
forensics: 6
(none): 2
misc: 1
[Tags outside the allowed list] 2
2026-07-writeup-07.md -> tag 'webb'
2026-41-writeup-41.md -> tag 'reversing'
[Broken internal links/images] 3
2026-05-writeup-05.md -> images/ctf8-web3.png
2026-19-writeup-19.md -> ./old-writeup.md
2026-47-writeup-47.md -> images/bof-flow.png
How to read it: the three blocks are each a tidy-up work list. ① the (none) 2 in the category distribution are pieces with no category in the front matter — they fall out of the per-field index, so fill them in. ② tags outside the allowed list — webb is a typo; reversing should be unified to rev. When tags scatter, "see all of this person’s web posts" stops working. ③ 3 broken links — image path typos and internal links pointing at deleted pieces. The moment a visitor sees a broken image, trust ends right there.
Run it yourself in your own folder. The counts will differ, but the structure is the same — the script gives you coordinates, and you fix only those coordinates.
3-3. Selecting 5 Representative Pieces and the Intro Page
Pick 5 of the 50 by 2-2’s criteria table. When selection is done, place them on the blog’s first screen (or a pinned page). Attach a one-line recommendation to each — a sentence answering "why read this one first" on the visitor’s behalf.
Output example (the representative list on the intro page):
■ Representative Write-ups (pinned page)
1. [pwn] babyheap — the day I first "read" the heap (the conclusion of the 10-problem heap drill series)
2. [web] jwt-forge — when alg=none doesn't work, the process of finding the next move
3. [crypto] rsa-fault — the power of one line of gcd; intro to the common-factor attack
4. [rev] rev-me-gently — four hours with anti-debugging, a record of 3 discarded hypotheses
5. [forensics] packet-river — the standard sequence for pulling credentials out of packets
How to read it: the fields are spread out (balance), and the one-liner beside each title advertises "there’s a narrative here." Balance’s role is making sure any visitor, whatever their field, picks up at least one.
Series bundles go on the same page too — as link lists like "competition #1–#8 growth record" and "the heap drill series." Representative pieces handle first impressions; series handle dwell time.
3-4. The About Page — A Self-Introduction Written in Numbers
Write the About page not as an emotional intro but as a list of verifiable facts. The formula for a self-introduction that gets read in hiring and networking is three blocks.
■ About page (example)
Journey: January 2026, first PowerShell steps → September, 8 CTF competitions entered (day 294 total)
Competition record: CTFtime team page link, best result top 40% (competition #7)
Tech stack: web (main) / pwn (secondary) / Python, pwntools, Burp Suite
Write-ups: 50 accumulated — see the list above for representative pieces
Contact: Discord ID or email (only what you can make public)
Two rules — only verifiable numbers (write only ranks that remain on record at CTFtime), anonymized personal information (a handle instead of real name/affiliation; a competition account for contact). A blog is a public place and also an information-collection target for attackers — the blog of someone studying security must not leak information first.
3-5. The Stats Dashboard — One Rank-Trend Table
Finally, make a table of per-competition rank trends and attach it to the About or representative-pieces page. Graphs get detailed treatment in the next steps (Step 297); today a table is enough.
| comp | rank | percentile | solved | notes |
|------|---------|------------|--------|-------|
| #1 | 291/380 | top 77% | 2 | first competition — the goal was finishing |
| #3 | 178/356 | top 50% | 4 | the team's first entry |
| #6 | 201/433 | top 46% | 5 | the midterm — held 50% |
| #8 | 158/395 | top 40% | 5 | the rotation experiment |
How to read it: to a visitor this table reads as one sentence — "bumpy, but trending up." And that one sentence backs 2-1’s "this person doesn’t stop" with data. Write only exact numbers, and leave blanks for competitions with no record — numbers filled by guesswork become data even you can’t trust later.
4. Missions & Exercises
Mission — 50 Accumulated + Blog Portfolio v1
- Complete 3 debrief tables via the competition #8 debrief (block A routine), and write write-ups from them to reach 50 accumulated.
- Save
blog_audit.pyand run it against your posts folder — organize the output’s three blocks (categories, tags, broken links) into a work list. - Fix every defect in the audit results and re-run to confirm "tags outside the allowed list: 0, broken links: 0."
- Select 5 representative pieces by 2-2’s criteria table, attach one-line recommendations, and place them on the intro page — if fields clump to one side, re-pick.
- Write the About page in the three-block structure (journey, record, stack) — review for unverifiable numbers and un-anonymized personal information.
- Make the competition rank-trend table and publish it — keep the whole tidy-up within the half-day timebox.
Exercises
Exercise 1. Explain why 50 write-ups reads as "the baseline of a consistently active player," connecting it to the three things a recruiter checks on a blog.
Exercise 2. Why is "narrative (the presence of trial-and-error and discarded hypotheses)" included in the representative-piece criteria? Answer together with why a success-path-only piece is unsuitable as a representative piece.
Exercise 3. When automating the tidy-up audit with a script, give two examples each of defects machines can find and defects humans must find.
Exercise 4. Why does the About page need the rule "write only verifiable numbers"? Include what problems inflated numbers create later.
5. Model Answers & Completion Criteria
Mission Model Answer
Verify against these criteria.
- 3 debrief tables: are all three boxes filled — "where I reached | the answer’s next step | what was needed"? A debrief that ends with "I read the write-up and understood" is not block A. For reproducible problems, there must be a record of solving to the end.
- 50 accumulated: is the blog’s actual piece count 50 or more, verifiable via the script’s
Accumulated write-ups:output? - 0 defects: does the re-run show "tags outside the allowed list: 0, broken internal links/images: 0" — category
(none)must also be 0 for the index to be complete. - 5 representative pieces: do they pass all four criteria (difficulty, narrative, reproducibility, field balance), each with a one-line recommendation?
- About page: are the three blocks (journey, record, stack) present, every number verifiable at CTFtime etc., and contact info separated into a competition account?
- Timebox: was the tidy-up actually done within half a day — if you did off-list work like a theme change, that’s the first thing to retrospect.
Exercise Answers
Answer 1. Because an accumulation of 50 cannot be forged in a short time. The three things a recruiter checks — consistency (posting intervals), growth (quality difference between old and current pieces), communicability (reproducible writing) — all show only "when many pieces are laid out along a time axis." Three well-written pieces can be produced with concentrated effort, but a steady interval and an upward trend across 50 pieces are impossible without routine. So 50 reads less as proof of skill than as proof of persistence — and in practical hiring, the latter is the rarer signal.
Answer 2. Because only a piece with narrative shows "how this person thinks." A write-up holding only the success path is close to a copy of an answer key — the reader can’t learn the author’s judgment. A piece with discarded hypotheses and their discard rationales, by contrast, reveals "what kind of person they are when stuck" — which is what a hiring or networking counterpart is really curious about. A representative piece should be not a billboard of skill but a sample of thinking.
Answer 3. Examples of defects machines find: missing categories/tags and inconsistent notation, broken internal links/image paths (what today’s script caught) — things judgeable by patterns and existence. Examples of defects humans find: the narrative quality and reproducibility of representative pieces, the About page’s tone and information scope — things requiring judgments of "is it good / is it appropriate." The dividing criterion is can the verdict rule be written as a sentence — if yes, it goes to the machine; if not, it’s the human’s share.
Answer 4. Because a portfolio’s numbers can be checked by the other side, and once one falsehood is exposed, everything else falls under suspicion. CTFtime ranks are public records that show up immediately when cross-checked, and they collapse at a single interview question like "what did you solve at this competition?" Conversely, verifiable numbers build trust the more conservative they are — the next sentence of a person whose "top 40%" checks out gets read without suspicion. A portfolio’s essence is not self-promotion but the accumulation of verifiability.
Completion Criteria Checklist
- [ ] I completed 3 competition #8 debrief tables in the three-box structure
- [ ] I reached 50 write-ups accumulated and confirmed it with script output
- [ ] I ran
blog_audit.pyon my blog and extracted the defect list - [ ] I fixed all missing categories, tag inconsistencies, and broken links and confirmed 0
- [ ] I selected 5 representative pieces by the four criteria and placed them with one-line recommendations
- [ ] I wrote the About page in the three-block structure (verifiable numbers, anonymized personal info)
- [ ] I published the competition rank-trend table
- [ ] I finished the tidy-up within the half-day timebox
6. Common Pitfalls & Fixes
Wall 1. I ran the script and got an argument error
Symptom:
root = Path(sys.argv[1])
~~~~~~~~^^^
IndexError: list index out of range
Cause: you didn’t pass the posts folder path as an argument. This script takes the folder via sys.argv[1] (measured).
Fix: run it in the form python blog_audit.py your_posts_folder. If the path has spaces, wrap it in double quotes — python blog_audit.py "my blog/posts". Dragging the folder from Windows Explorer into the terminal enters the path automatically.
Wall 2. The audit says 0 defects but something’s off — it found no pieces at all
Symptom: it shows Accumulated write-ups: 0.
Cause: mostly you pointed one level above/below the right folder, or the pieces have a different extension (.markdown, .txt). The script only looks at *.md.
Fix: first check inside the folder with ls *.md (Git Bash) or dir *.md (PowerShell) whether files show up. If pieces are spread across subfolders, change root.glob("*.md") to root.rglob("*.md") for recursive search.
Wall 3. I picked representative pieces and they’re all web
Symptom: well-written pieces clump in your main field, so the 5 pieces’ field balance won’t work.
Cause: it’s natural that main-field pieces are higher in absolute quality — that’s where you spent the time.
Fix: don’t relax the balance rule; reverse the selection order — first pick one piece each from the secondary fields, the ones with the most alive narrative, then fill the remaining slots with main-field pieces. If the secondary-field pieces look weak, that’s the improvement assignment for the next write-ups. Representative selection is a judgment of the past and a writing plan for the future.
Wall 4. While tidying I started changing the theme — the half-day is gone
Symptom: you started picking fonts and colors, and haven’t fixed a single piece.
Cause: you put tidying and decorating in one basket. Decorating has no end and gives instant gratification, which makes it more dangerous.
Fix: write the work list on paper, and the moment you catch yourself doing something not on the list, stop immediately. Note the "I want to change the theme" urge and schedule it separately after the timebox ends. The tidy-up’s completion condition is only one — the script re-run shows 0 and the representative pieces are placed.
Wall 5. I counted and it’s 47 — how do I fill the number?
Symptom: competition write-ups alone don’t reach 50.
Cause: normal. Writing only competition problems ties the count to the number of competitions.
Fix: as you did in Step 286, turn HTB/THM machine solves into write-ups — but machines must be confirmed retired before publishing. Problems you reproduced while debriefing also make excellent write-ups once polished. Only padding the count by force is forbidden — a piece that fails the "could the me of 6 months from now reproduce with this piece" standard (Step 286) protects the portfolio better by not being counted.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| The 50 baseline | An unforgeable accumulation — triple proof of consistency, growth, communicability |
| The reader’s 30 seconds | The time a visitor gives — representative pieces, not the latest, must reach them |
| Representative 4 criteria | Difficulty, narrative, reproducibility, field balance — picking samples of thinking |
| Tidying’s two layers | Defects machines find (patterns) / defects humans find (judgment) |
| Timebox | Tidying is half a day — no off-list work; decorating gets a separate schedule |
| Verifiability | Portfolio numbers only if cross-checkable — the body of trust |
Today’s Tools & Commands
| Tool/command | What it does |
|---|---|
python blog_audit.py <folder> |
Batch audit of category distribution, tag consistency, broken links |
ALLOWED_TAGS set |
The reference list for unified tag notation — fixed as a team rule |
| Representative intro page | The first screen receiving the visitor’s 30 seconds — one-line recommendations mandatory |
| About three blocks | Journey, record, stack — verifiable numbers + anonymized contact |
| Rank-trend table | The one page backing "doesn’t stop" with data |
The Core Instinct
What you did today looks like tidying on the outside, but its essence is a perspective switch. Until now the blog was "review notes for future me" — from today it’s also "a document by which others evaluate me." The same writing demands a different quality once it gains readers.
And this document is a living thing. Through competitions #9 and #10 the representative pieces get renewed and the trend table grows longer. Tidying is not a one-time event but part of the accumulation routine — at the next milestone (Step 297’s 10-competition retrospective) this portfolio goes back on the judging stand. Today’s half-day was preparation for that day.
Once every box is checked, Step 294 is complete. Click the checkbox in the sidebar to save your progress.