Page MenuHomedesp's stash

No OneTemporary

diff --git a/ctf-diary-generator/generator.py b/ctf-diary-generator/generator.py
index b386f47..fdc7b0a 100644
--- a/ctf-diary-generator/generator.py
+++ b/ctf-diary-generator/generator.py
@@ -1,206 +1,207 @@
import yaml, os, re
from datetime import datetime
from markdown import markdown
from bs4 import BeautifulSoup, Comment
#also requires pymdown-extensions
#note to self: to convert existing ctf diaries, check whether `\n([0-9a-zA-Z]+)` (or `^((?:[a-zA-Z0-9] ?)+)\n- ` for some messier writeups) looks like a chall name, then replace with `\n### $1`
#remove all first level -s that i use for paragraph into 2 \ns
#push all ```s to the left without spaces
#TODO aggregate stats (total ctfs, total solves, solve and co-solve amt, category solves breakdown, avg chall per ctf?, avg points?, avg solve count??)
#exclude organized events
CWD = os.path.dirname(os.path.realpath(__file__))
alphanum = re.compile(r'[^A-Za-z0-9\- ]+')
merge_space = re.compile(r'\s+')
remove_formatting = lambda str: merge_space.sub('-', alphanum.sub('', str).strip()).lower() #for name formats
objects = {}
comments = {}
#read special events
with open(f'{CWD}/ctf-diary/special.yml', encoding='utf-8') as s:
objects.update(yaml.load(s, Loader=yaml.Loader))
#read all ctfs in record
skipped_cmts = []
for file in os.listdir(f'{CWD}/ctf-diary/ctfs'):
if file.endswith('.yml'):
name = file[:-4]
#read metadata into objects
with open(f'{CWD}/ctf-diary/ctfs/{file}', encoding='utf-8') as s:
objects.update({name: yaml.load(s, Loader=yaml.Loader)})
#read comments; split according to headers ('### ') which should only be used by chall names, where the - is the start of comments
#the 3 ### should be good enough to differentiate headers from code comments, we will do sanity check when we actually map the comments anyway
try:
with open(f'{CWD}/ctf-diary/ctfs/comments/{name}.md', encoding='utf-8') as c:
assert c.read(4) == '### ' #the first line should already have a challenge, also discard the header
comments.update({name: {remove_formatting((sp:=v.split('\n', 1))[0]): sp[1] for v in c.read().split('\n### ')}})
except Exception as e:
skipped_cmts.append(name)
if 'challenges' in objects[name] and not all('writeup-url' in chall and chall['writeup-url'] for chall in objects[name]['challenges']): #only print if ctf should have comments
print(f'Cannot read comments for {file} ({type(e).__name__}), skipping...')
#date is already datetime.date (thanks pyyaml)
objects = dict(sorted(objects.items(), key=lambda item: item[1]['date']))
#add year objects
year = None
templist = []
for k, v in objects.items():
if year != v['date'].year:
year = v['date'].year
if templist: #ensure templist is not empty before inserting a year (since that always happen)
#no need to str(year) since .format nudge it into string anyway
templist.append((year, {'style': 'timeline-year', 'content': year, 'date': datetime.fromisoformat(f'{year}-01-01')}))
templist.append((k,v))
#apparently printing dicts autosorts it for you but iterating is fine
templist.reverse()
objects = dict(templist)
with open(f'{CWD}/templates/ctf.html', encoding='utf-8') as cf:
ctf = cf.read()
with open(f'{CWD}/templates/special.html', encoding='utf-8') as sf:
special = sf.read()
with open(f'{CWD}/templates/diary.html', encoding='utf-8') as df:
diary = df.read()
#generation helpers
added = []
def get_comment(chall, ctf):
name = remove_formatting(next(iter(chall.values())))
if 'writeup-url' in chall and chall['writeup-url']:
return f'href="{chall["writeup-url"]}"'
elif ctf in comments and name in comments[ctf]:
added.append(f'{ctf}-{name}')
return f'data-comment="{ctf}-{name}"'
else:
if ctf not in skipped_cmts: #only print if the file is read, otherwise its just redundant
print(f"{ctf}-{name} has no comments, name mismatch?")
return ''
ranks = {1: 'first', 2: 'second', 3: 'third'}
def get_ordinal(n):
if n < 0:
return '<div>N/A</div>'
else:
formatted = f"{n}<sup>{'th' if 11 <= (n % 100) <= 13 else ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)]}</sup>"
if n <= 3:
return '<div class="text-' + ranks[n] + '">' + formatted + '</div>'
else:
return '<div>' + formatted + '</div>'
special_field_names = {
'name': '<th scope="col">Challenge <a class="text-light" data-toggle="modal" data-target="#help" href="#help" role="button"><i class="fa fa-question-circle"></i></a></th>',
'writeup-url': '<th scope="col">Full writeup</th>',
#special fields that are not actually fields
'first-blood': '',
'writeup-prize': '',
}
chall_decor = {'first-blood': '🩸', 'writeup-prize': '👑'}
special_fields = {
#for now instead of bolding first item in row, just bold these 2 since i dont think ill need another name for the challenges anyway
#we are still assuming the name is in the first column though - commenting breaks if not
'name': lambda v,c: f'<th scope="row">{v} {" ".join([v for k, v in chall_decor.items() if k in c])}</th>',
'challenge-written': lambda v,_: f'<th scope="row">{v}</th>',
'writeup-url': lambda v,_: '<td>' + ('✔️' if v else '❌') + '</td>',
'solve-status': lambda v,_: '<td>' + {'solved': '✔️', 'co-solved': '🤝', 'sniped': '💤', 'unsolved': '👀'}[v] + '</td>',
#special fields that are not actually fields
'first-blood': lambda v,_: '',
'writeup-prize': lambda v,_: '',
}
#generate the page following the template formats
html = diary.format(
ctfs="".join([
#special
special.format(style=v['style'], content=v['content'])
if 'content' in v else
#actual ctfs
ctf.format(
+ id=k,
style='timeline-organized' if v['organizer'] else '',
#class="headings" to have same style but not clickable if url is null, otherwise link
#also link is non w3c compliant hack around interactive elements inside buttons; who complies anyway :)
url=f'class="nav-link" onclick="window.location=\'{v["url"]}\'; event.stopPropagation()"' if v['url'] else 'class="headings"',
name=v['name'],
#isoformat is the one we want for datetime, but we only need date not time; actual format should be <month-abbr>. <day>
date=f'<time datetime="{v["date"].isoformat().split("T")[0]}">{v["date"].strftime("%b. %d")}</time>',
duration=f'{v["duration"]}h', #currently we are hardcoding hours, but we can always parse
type=v['type'],
team=v['team'],
rank='<div class="text-success">Organizer</div>'
if v['organizer'] else
(('<div>' + v['rank'] + '</div>' if isinstance(v['rank'], str) else get_ordinal(v['rank'])) +
('<span data-toggle="tooltip" title="All challenges cleared">✨</span>' if v['full-clear'] else '')),
#use the first challenge as header definition
challengeheader='<thead><tr>'
+ ''.join([
#normal fields that are named as expected so we can just use it as the headers
'<th scope="col">' + name.replace('-', ' ').capitalize() + '</th>'
if name not in special_field_names else
#special fields that needs renaming
special_field_names[name]
for name in v['challenges'][0].keys()])
+ '</tr></thead>'
if 'challenges' in v else "<div class='text-center'>No specific challenges have been logged; It's all a team effort!</div>", #allow no challenge specified (e.g. A/D ctfs where its basically fully team effort so no specific challs that i wouldve fully solved)
challenges=''.join(['<tbody><tr ' + get_comment(chall, k) + '>'
#assume every chall object follows the same format as the first, or else header mismatches
+ ''.join([
f'<td>{field}</td>'
if name not in special_fields else
special_fields[name](field, chall)
for name, field in chall.items()])
+ '</tr><tbody>'
for chall in v['challenges']])
if 'challenges' in v else '',
)
for k, v in objects.items()])
)
#lint output
soup = BeautifulSoup(html, 'html.parser')
for comment in soup.findAll(text=lambda text:isinstance(text, Comment)):
comment.extract()
with open('ctf.html', 'w', encoding="utf-8") as out:
out.write(str(soup)) #minify
#write comments into their respective files
if not os.path.exists('ctf'):
os.mkdir('ctf')
for ctf, challs in comments.items():
for name, cmt in challs.items():
with open(f'ctf/{ctf}-{name}.html', 'w', encoding="utf-8") as out:
#out.write(markdown(cmt, extensions=['fenced_code', 'codehilite'], extension_configs={'codehilite': {'noclasses': True}}))
out.write(markdown(cmt,
extensions=['pymdownx.highlight', 'pymdownx.superfences', 'pymdownx.tilde', 'pymdownx.inlinehilite', 'pymdownx.emoji', 'pymdownx.magiclink'],
extension_configs={
'pymdownx.highlight': {
'guess_lang': 'block',
'pygments_lang_class': True,
},
}))
#sanity check if we missed any challs in the yml by comparing against added comments
if f'{ctf}-{name}' not in added:
print(f"{ctf}-{name} has a comment but doesn't exist in {ctf}.yml - missed definition?")
\ No newline at end of file
diff --git a/ctf-diary-generator/templates/ctf.html b/ctf-diary-generator/templates/ctf.html
index ea017fc..9f66a50 100644
--- a/ctf-diary-generator/templates/ctf.html
+++ b/ctf-diary-generator/templates/ctf.html
@@ -1,32 +1,32 @@
<!--row needed to correctly align ctf-table-->
-<div class="text-right info-subsegment ctf-info row">
+<div id="{id}" class="text-right info-subsegment ctf-info row">
<div class="btn-group timeline-anchor {style}">
<!--dropdown-toggle solely for the arrow-->
<button type="button" class="btn text-white text-left dropdown-toggle" data-toggle="collapse" aria-expanded="false">
<div class="ctf-title">
<div {url}>
{name}
</div>
</div>
<div class="ctf-metadata">
<i>{date} <div class='big-sep'></div> {duration} <div class='big-sep'></div> {type}</i>
</div>
</button>
<div class="ctf-details">
{rank}
<div class='big-sep'></div>
<div>{team}</div>
</div>
</div>
<div class="collapse ctf-table">
<div class="table-responsive">
<table class="table table-hover text-left table-dark">
<col style="width:40%"> <!--widen challenge name-->
{challengeheader}
{challenges}
</table>
</div>
</div>
</div>
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 21, 7:15 PM (1 d, 19 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
53/95/f55100485b04c18e8f005dbb363c

Event Timeline