Last active
April 6, 2024 04:33
-
-
Save ngregoire/cbddac55bf8ba25965302e3bbf75dc31 to your computer and use it in GitHub Desktop.
Matplot script used to generate timelines
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The script requires Python3 and the packages `numpy`, `pandas` and `matplotlib`. | |
It accepts a input file compatible with Mermaid (cf `bb.data`) and generates a PNG file. | |
The tag `<br/>` is supported, so that a label can be displayed on several lines. | |
I use the font `Humor Sans`, that can be installed via `apt install fonts-humor-sans`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
timeline | |
title Bug Bounty | |
2002 : iDefense | |
2004 : Firefox | |
2005 : ZDI | |
2007-03-01 : My first<br>ZDI report | |
2010 : Google | |
2011 : Facebook | |
2012 : HackerOne | |
2012-09-10 : Bugcrowd | |
2013-10-01 : Yahoo's<br> "t-shirt gate" | |
2014-01-01 : My first<br>Web bounty | |
2015 : YesWeHack | |
2016 : Intigriti | |
2016-08-08 : H1-702 in Vegas | |
2016-12 : Uber's "ransom" | |
2019-03 : Santiago Lopez<br>reaches $1M<br>in bounties (on h1) | |
2020-12 : Immunefi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import sys | |
import numpy as np | |
import pandas as pd | |
import matplotlib.pyplot as plt | |
import matplotlib.dates as mdates | |
############# | |
# CONSTANTS # | |
############# | |
STYLES = { | |
"timeline": "solid", | |
"marker": "o", | |
"label_alignment": "center", | |
"x_alignment": "center", | |
"x_rotation": 30, | |
} | |
COLORS = { | |
"timeline": "black", | |
"marker": "black", | |
"stem": "black", | |
"label": "olive", | |
"x_label": "salmon", | |
"x_ticks": "black", | |
} | |
ENDS = { | |
"left": {"event": "25 years ago", "date": "15/03/1999"}, | |
"right": {"event": "Now", "date": "15/03/2024"}, | |
} | |
############# | |
# FUNCTIONS # | |
############# | |
def read_data(fname): | |
events = [] | |
dates = [] | |
try: | |
# Read the file | |
lines = open(fname, mode="r").readlines() | |
# Extract title | |
title = lines[1].split("title ")[1].strip() | |
# Process data points | |
for data_point in lines[2:]: | |
# Remove comments | |
data_point = data_point.split("#", 1)[0].strip() | |
# Skip empty lines | |
if data_point=="": | |
continue | |
# Split by the first colon | |
when, what = data_point.split(":", 1) | |
# Add to arrays | |
events.append(what.strip()) | |
dates.append(pd.Timestamp(when.strip())) | |
# Add fixed data points | |
events = [ENDS["left"]["event"]] + events + [ENDS["right"]["event"]] | |
dates = [pd.Timestamp(ENDS["left"]["date"])] + dates + [pd.Timestamp(ENDS["right"]["date"])] | |
# Catch exceptions | |
except ValueError as e: | |
print(f"[!] Can't process data point '{data_point.strip()}' from file '{fname}'\nError:", e) | |
exit(-1) | |
except FileNotFoundError as e: | |
print(f"[!] Can't read file '{fname}'\nError:", e) | |
exit(-1) | |
except PermissionError as e: | |
print(f"[!] Can't access file '{fname}'\nError:", e) | |
exit(-1) | |
# Return the collected data | |
return title, pd.DataFrame({ 'event': events, 'date': dates }).sort_values(by="date") | |
def validate_indexes(dataframe): | |
indexes_ok = True | |
expected_i = 0 | |
for i in dataframe.index: | |
if i != expected_i: | |
print(f"Index {i} should be {expected_i}") | |
indexes_ok = False | |
expected_i = expected_i + 1 | |
if indexes_ok: | |
print("Indexes are OK") | |
else: | |
print("Indexes aren't correctly sorted") | |
############# | |
# MAIN CODE # | |
############# | |
# Read the text file describing the timeline | |
src_file = sys.argv[1] | |
fig_name, df = read_data(src_file) | |
print(f"==== DataFrame\n{df=}") | |
# Check if the data is correctly sorted | |
print("==== Validating indexes") | |
validate_indexes (df) | |
# Define levels for labels | |
levels = np.tile( | |
[ -3, 3, -2, 2, -1, 1], | |
int(np.ceil(len(df)/6)) | |
)[:len(df)] | |
# Use the XKCD theme | |
print("==== Generating plot") | |
with plt.xkcd(length=100.0, randomness=3.0): | |
# Create the Matplot figure | |
fig, ax = plt.subplots(figsize=(12.8,7.2), constrained_layout=True) | |
# Define margins | |
ax.margins(y=0.1) | |
# Draw the timeline itself | |
ax.plot( | |
df['date'], | |
np.zeros_like(df['date']), | |
marker=STYLES["marker"], | |
linestyle=STYLES["timeline"], | |
color=COLORS["timeline"], | |
markerfacecolor=COLORS["marker"], | |
) | |
# Draw the vertical stems | |
ax.vlines(df['date'], 0, levels, color=COLORS["stem"]) | |
# Add labels | |
for date, level, event in zip(df['date'], levels, df['event']): | |
ax.annotate( | |
event.replace("<br>", "\n"), | |
color=COLORS["x_label"] if event in [ENDS["left"]["event"], ENDS["right"]["event"]] else COLORS["label"], | |
xy=(date, level), | |
xytext=(-3, np.sign(level)*3), | |
textcoords="offset points", | |
ha=STYLES["label_alignment"], | |
va="bottom" if level > 0 else "top" | |
) | |
# Format the x-axis | |
## Major | |
ax.xaxis.set_major_locator(mdates.YearLocator(5)) | |
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y")) | |
ax.tick_params(which='major', length=10, width=2, color=COLORS["x_ticks"]) | |
## Minor | |
ax.xaxis.set_minor_locator(mdates.YearLocator(1)) | |
ax.tick_params(which='minor', length=5, width=2, color=COLORS["x_ticks"]) | |
# Add labels to the x-axis | |
plt.setp(ax.get_xticklabels(), color=COLORS["x_label"], rotation=STYLES["x_rotation"], ha=STYLES["x_alignment"]) | |
# Remove y-axis and spines | |
ax.yaxis.set_visible(False) | |
ax.spines["left"].set_visible(False) | |
ax.spines["top"].set_visible(False) | |
ax.spines["right"].set_visible(False) | |
# Save to disk | |
dst_file = src_file.replace(".data", ".png") | |
plt.savefig(dst_file) | |
print("==== Done") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment