Skip to content

Instantly share code, notes, and snippets.

@jartigag
Last active November 13, 2021 18:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jartigag/3984755268cc2780fa4fa8b588155267 to your computer and use it in GitHub Desktop.
Save jartigag/3984755268cc2780fa4fa8b588155267 to your computer and use it in GitHub Desktop.
hackmd2writeup.py
#!/usr/bin/env python3
#
# this script adapts a hackmd note **including images** to the Scavenger's writeup style:
# https://scavengersecurity.com/
# usage and output:
# $ ./hackmd2writeup.py test.md
# OR
# $ ./hackmd2writeup.py hb5VSfHnQNKbdHbOCftmCA
# [+] __mandatory fields__:
# date: 2021-09-12
# event: CSAW
# title: Gotta Decrypt Them All
# discipline: crypto
# author: jartigag, blast_o
#
# [?] the post 2021-09-12-csaw-gottadecryptthemall will be created. is this format correct? [Y/n]
#
# [+] writeup created in jekyll/_posts/2021-09-12-csaw-gottadecryptthemall/2021-09-12-csaw-gottadecryptthemall.md
# example:
# $ head hackmd_draft_note.md
# ---
# event: CSAW
# date: 2021-09-12
# title: Gotta Decrypt Them All
# discipline: crypto
# difficulty: easy - 235 solves (175 points)
# tags: morse, base64, rsa, rot13
# usemathjax: true
# author: jartigag, blast_o
# ---
# $ head jekyll/_posts/2021-09-12-csaw-gottadecryptthemall/2021-09-12-csaw-gottadecryptthemall.md
# ---
# author: jartigag, blast_o
# categories: ctf
# date: 2021-09-12
# layout: post
# tags:
# - jartigag
# - blast_o
# - morse
# - base64
# - rsa
# - rot13
# title: CSAW - Gotta Decrypt Them All [Crypto]
# usemathjax: true
# ---
# additional setup instructions:
#
# ___ how to setup a hackmd template:
# $ cp default-hackmd-template.md $HACKMD_DIR/public/default.md
#
# ___ how to export from a hackmd note to a markdown file:
#
# # 1. get the cli:
# $ wget https://raw.githubusercontent.com/hedgedoc/cli/\
# 8b13b8836cf330921856907d905421d34a1e645c/bin/hedgedoc -O hackmd && chmod +x hackmd
#
# # 2. set config via environment variables:
# $ export HEDGEDOC_SERVER='https://hackmd.scavengersecurity.com'
#
# # 3. export by note_id:
# $ ./hackmd export --md G2wBk0sAQQqqsO2jFZrgrA note.md
__author__ = "@jartigag"
__version__ = '1.4'
# CHANGELOG:
# v1.4 (20211102):
# - fix empty tags bug (by @00xc1)
#
# v1.3 (20211102):
# - post title standardization (by @00xc1)
#
# v1.2 (20211024):
# - ctrl+c signal handled
# - improvements in hackmd_cli error handling
#
# v1.1 (20211024):
# - export to md from note_id
import sys, os
from pathlib import Path
import shutil
import yaml
from datetime import datetime
import signal
HACKMD_UPLOAD_DIR = Path('/opt/docker/hackmd/upload-data/')
JEKYLL_DIR = Path('/var/jekyll/scavenger/')
HACKMD_CLI = '/home/jartigag/hackmd'
HEDGEDOC_SERVER="https://hackmd.scavengersecurity.com"
os.putenv('HEDGEDOC_SERVER',HEDGEDOC_SERVER)
def bold(text): return f"\033[1m{text}\033[0m"
def get_yaml_header(f):
"""extracted from https://stackoverflow.com/q/57168926"""
pointer = f.tell()
if f.readline() != '---\n':
f.seek(pointer)
return ''
readline = iter(f.readline, '')
readline = iter(readline.__next__, '---\n')
return ''.join(readline)
def make_it_a_list(d,k):
"""example: k='author' -> get 'author'/'authors' tag as a list,
being that a string or a list"""
author_list = d.get(k,d.get(f"{k}s",''))
if isinstance(author_list,str):
return [author for author in author_list.split(", ") if author]
else:
return author_list
def draft2writeup_header(d):
return dict({
'layout': 'post',
'title': f"{d['event']} - {d['title']} [{d['discipline'].capitalize()}]",
'categories': 'ctf',
'tags': make_it_a_list(d,'author')+make_it_a_list(d,'tag'),
'date': d['date'],
'author': ", ".join(make_it_a_list(d,'author'))
}, **{k: d[k] for k in d.keys()-['title', 'event','discipline','tag','tags','difficulty','author','authors']})
#TODO: use 'difficulty' tag for something
def sigint_handler(signal, frame):
print("\n[!] ERROR: process cancelled")
sys.exit(0)
if __name__ == '__main__':
signal.signal(signal.SIGINT, sigint_handler)
if len(sys.argv)==2 or (len(sys.argv)==3 and sys.argv[2]=="-y"):
draft_file = sys.argv[1]
if not '.md' in draft_file:
# so draft_file is a note_id:
note_id = draft_file
os.environ['HEDGEDOC_SERVER'] = HEDGEDOC_SERVER
exit_code = os.system(f"{HACKMD_CLI} export --md {note_id} note.md 1>/dev/null")
if exit_code!=0:
sys.exit(f"[!] ERROR: 'export HEDGEDOC_SERVER={HEDGEDOC_SERVER}; {HACKMD_CLI} export {note_id} note.md")
if open('note.md').readlines()[0]=='<!DOCTYPE html>\n':
os.system('rm note.md')
sys.exit(f"[!] ERROR: '{draft_file}' is invalid as a file (must be .md) and as a note_id, since is not found in {HEDGEDOC_SERVER} ( {HEDGEDOC_SERVER}/{note_id} )")
print(f"[+] note_id {note_id} exported as note.md")
draft_file = 'note.md'
draft_header_str = get_yaml_header(open(draft_file))
y = yaml.safe_load(draft_header_str)
# 0. CHECK MANDATORY FIELDS: date, event, title, discipline, author/authors
print('[+] __mandatory fields__:')
all_mandatory_fields = True
for f in ['date', 'event', 'title', 'discipline']:
print(f"{f}: {y.get(f,'')}")
if f not in y:
all_mandatory_fields = False
print(f"author: {', '.join(make_it_a_list(y,'author'))}")
if not make_it_a_list(y,'author'):
all_mandatory_fields = False
if not all_mandatory_fields:
sys.exit('[!] ERROR: mandatory field(s) missing')
# 1. GET DIR/POST NAME AND CREATE DIR:
lcase_event = y['event'].lower().replace(' ','')
lcase_title = y['title'].lower().replace(' ','')
post_dirname = f"{y['date']}-{lcase_event}-{lcase_title}"
if len(sys.argv)==2:
answ = input(f"\n[?] the post {bold(post_dirname)} will be created. is this format correct? [Y/n] ")
else:
if sys.argv[2]=="-y":
answ="y"
if answ=="": answ="y"
if answ[0].isnumeric() or answ[0].lower()!="y":
post_dirname = ''
while len(post_dirname.split('-'))!=5:
post_dirname = input("\n[?] please correct the post info, in YYY-MM-DD-event-title format: ")
y['date'] = datetime.strptime("-".join(post_dirname.split('-')[0:3]),"%Y-%m-%d").date()
y['event'] = post_dirname.split('-')[3]
y['title'] = post_dirname.split('-')[4]
for k in ('event','title'):
answ = input(f"\n[?] the {k} will be {bold(y[k])}. press intro if it's correct or rewrite it here: ")
if answ!="": y[k]=answ
lcase_event = y['event'].lower().replace(' ','')
lcase_title = y['title'].lower().replace(' ','')
post_dirname = f"{y['date']}-{lcase_event}-{lcase_title}"
post_dir = Path(JEKYLL_DIR,"_posts",post_dirname)
post_dir.mkdir(exist_ok=True)
# 2. GENERATE NEW YAML METADATA:
new_y = draft2writeup_header(y)
writeup_header_str = yaml.dump(new_y)
# 3. COPY PNGS AND DUMP THE WRITEUP CONTENT:
draft_str = open(draft_file).read()
for png in HACKMD_UPLOAD_DIR.iterdir():
exit_code = os.system(f"grep {png.name} {draft_file}")
if exit_code==0:
shutil.copyfile(png,post_dir.joinpath(png.name))
draft_str = draft_str.replace(f"/uploads/{png.name}",png.name)
writeup_str = draft_str.replace(draft_header_str, writeup_header_str)
with open(post_dir.joinpath(f"{post_dirname}.md"),'w') as writeup_file:
writeup_file.write(writeup_str)
print(f"\n[+] writeup created in {bold(post_dir.joinpath(post_dirname+'.md'))}")
else:
script_name = sys.argv[0].strip('./')
print(f"\n[!] usage: ./{script_name} draft_file.md")
print(f"OR ./{script_name} note_id")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment