mirror of
https://github.com/NohamR/LetCTF.git
synced 2026-05-25 19:59:23 +00:00
first push
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -127,3 +127,5 @@ dmypy.json
|
|||||||
|
|
||||||
# node
|
# node
|
||||||
node_modules/
|
node_modules/
|
||||||
|
*.json
|
||||||
|
/writeups
|
||||||
|
|||||||
165
README.md
Normal file
165
README.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# CTF Writeup Generator
|
||||||
|
|
||||||
|
A Python tool to automatically generate CTF writeup templates and organize challenge files. Currently supports Hackropole platform with extensibility for other CTF platforms.
|
||||||
|
|
||||||
|
### Supported CTF Websites :
|
||||||
|
- https://hackropole.fr
|
||||||
|
|
||||||
|
### Will add :
|
||||||
|
- https://imaginaryctf.org
|
||||||
|
- https://theblackside.fr
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automated writeup template generation
|
||||||
|
- Challenge information scraping
|
||||||
|
- File downloading and organization
|
||||||
|
- Support for multiple CTF platforms
|
||||||
|
- Hugo-compatible markdown generation
|
||||||
|
- Multilingual support (English/French)
|
||||||
|
- Cookie-based and token-based authentication
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/nohamr/LetCTF.git
|
||||||
|
cd LetCTF
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- ## Configuration
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
The tool supports both cookie-based and token-based authentication. Create one or both of the following files:
|
||||||
|
|
||||||
|
1. `config.json` for token-based auth:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "your-token-here"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `cookies.json` for cookie-based auth:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session": "your-session-cookie",
|
||||||
|
"other_cookie": "other-cookie-value"
|
||||||
|
}
|
||||||
|
``` -->
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
from src.platforms.hackropole import HackropolePlatform
|
||||||
|
from src.generator import WriteupGenerator
|
||||||
|
|
||||||
|
# Initialize platform with auth if needed
|
||||||
|
platform = HackropolePlatform(
|
||||||
|
config_file="config.json",
|
||||||
|
cookie_file="cookies.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create generator
|
||||||
|
generator = WriteupGenerator(platform, Path("./writeups"))
|
||||||
|
|
||||||
|
# Generate writeups
|
||||||
|
generator.fetch_challenges()
|
||||||
|
generator.generate_writeup_structure(
|
||||||
|
hugo_header=True, # Include Hugo front matter
|
||||||
|
translated=True # Generate French translations
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
writeups/
|
||||||
|
├── Platform1/
|
||||||
|
│ ├── Challenge1/
|
||||||
|
│ │ ├── index.md
|
||||||
|
│ │ ├── index.fr.md
|
||||||
|
│ │ └── challenge_files/
|
||||||
|
│ └── Challenge2/
|
||||||
|
└── Platform2/
|
||||||
|
└── Challenge3/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Platforms
|
||||||
|
|
||||||
|
1. Create a new platform file in `src/platforms/`:
|
||||||
|
```python
|
||||||
|
from .base import CTFPlatform
|
||||||
|
from ..models import Challenge
|
||||||
|
|
||||||
|
class NewPlatform(CTFPlatform):
|
||||||
|
def login(self, credentials: Dict) -> bool:
|
||||||
|
# Implement login
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_challenges(self) -> List[Challenge]:
|
||||||
|
# Implement challenge fetching
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_challenge(self) -> Challenge:
|
||||||
|
# Implement specific challenge by URL
|
||||||
|
pass
|
||||||
|
|
||||||
|
def generate_template(self, challenge: Challenge, hugo_header: bool = False, translated: bool = False):
|
||||||
|
# Implement generating writeup template for challenge
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Use the new platform:
|
||||||
|
```python
|
||||||
|
from src.platforms.new_platform import NewPlatform
|
||||||
|
|
||||||
|
platform = NewPlatform()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Format
|
||||||
|
|
||||||
|
The generated writeup templates include:
|
||||||
|
- Challenge metadata (author, category, difficulty)
|
||||||
|
- Challenge description
|
||||||
|
- File information with hashes
|
||||||
|
- Basic writeup structure
|
||||||
|
- Hugo front matter (optional)
|
||||||
|
|
||||||
|
Example template:
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: "Challenge Name"
|
||||||
|
date: "1970-00-00T00:00:00"
|
||||||
|
tags: ["category", "badge1", "badge2"]
|
||||||
|
author: "Author"
|
||||||
|
---
|
||||||
|
|
||||||
|
- Author: Challenge Author
|
||||||
|
- Category: Web
|
||||||
|
- Difficulty: ⭐⭐⭐ (3/5)
|
||||||
|
- Challenge URL: [Challenge Name - Platform](https://platform/challenges/123)
|
||||||
|
- Files provided: [file.zip](https://url/file.zip) *(SHA256: hash)*
|
||||||
|
|
||||||
|
## Writeup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Commit your changes
|
||||||
|
4. Push to the branch
|
||||||
|
5. Create a Pull Request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
18
main.py
Normal file
18
main.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from src.platforms.hackropole import HackropolePlatform
|
||||||
|
from src.generator import WriteupGenerator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
challenge_url = 'https://hackropole.fr/fr/challenges/reverse/fcsc2023-reverse-chaussette-xs/'
|
||||||
|
# challenge_url = 'https://hackropole.fr/fr/challenges/crypto/fcsc2022-crypto-t-rex/'
|
||||||
|
# challenge_url = 'https://hackropole.fr/fr/challenges/crypto/fcsc2022-crypto-a-laise/'
|
||||||
|
|
||||||
|
def main():
|
||||||
|
platform = HackropolePlatform()
|
||||||
|
|
||||||
|
generator = WriteupGenerator(platform, Path("./writeups"))
|
||||||
|
generator.fetch_challenge(challenge_url=challenge_url)
|
||||||
|
print(generator.challenges)
|
||||||
|
generator.generate_writeup_structure(hugo_header=True, translated=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
beautifulsoup4==4.13.3
|
||||||
|
Requests==2.32.3
|
||||||
55
src/generator.py
Normal file
55
src/generator.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
from .platforms.base import CTFPlatform
|
||||||
|
from .models import Challenge
|
||||||
|
|
||||||
|
class WriteupGenerator:
|
||||||
|
"""Main class to handle writeup generation"""
|
||||||
|
def __init__(self, platform: CTFPlatform, output_dir: Path):
|
||||||
|
self.platform = platform
|
||||||
|
self.output_dir = output_dir
|
||||||
|
self.challenges = []
|
||||||
|
|
||||||
|
def fetch_challenges(self):
|
||||||
|
"""Fetch all challenges from the platform"""
|
||||||
|
self.challenges = self.platform.get_challenges()
|
||||||
|
|
||||||
|
def fetch_challenge(self, challenge_url: str) -> Challenge:
|
||||||
|
"""Fetch a specific challenge"""
|
||||||
|
self.challenges = [] if not self.challenges else self.challenges
|
||||||
|
challenge = self.platform.get_challenge(challenge_url)
|
||||||
|
self.challenges.append(challenge)
|
||||||
|
|
||||||
|
def generate_writeup_structure(self, hugo_header: bool = False, translated: bool = False):
|
||||||
|
"""Generate folder structure and writeup templates"""
|
||||||
|
for challenge in self.challenges:
|
||||||
|
platform_dir = self.output_dir / challenge.platform.lower()
|
||||||
|
platform_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
challenge_dir = platform_dir / self._sanitize_filename(challenge.id)
|
||||||
|
challenge_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
files_dir = challenge_dir / "files"
|
||||||
|
files_dir.mkdir(exist_ok=True)
|
||||||
|
self.platform.download_challenge_files(challenge, files_dir)
|
||||||
|
|
||||||
|
self.platform.generate_template(challenge, hugo_header, translated)
|
||||||
|
|
||||||
|
if (challenge_dir / "index.md").exists():
|
||||||
|
print(f"Writeup for {challenge.id} already exists. Skipping...")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
(challenge_dir / "index.md").write_text(challenge.template)
|
||||||
|
|
||||||
|
if translated:
|
||||||
|
if (challenge_dir / "index.fr.md").exists():
|
||||||
|
print(f"Writeup for {challenge.id} already exists. Skipping...")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
(challenge_dir / "index.fr.md").write_text(challenge.template_translated)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_filename(filename: str) -> str:
|
||||||
|
"""Sanitize filename for filesystem"""
|
||||||
|
return "".join(c for c in filename if c.isalnum() or c in (' ', '-', '_')).strip()
|
||||||
24
src/models.py
Normal file
24
src/models.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Challenge:
|
||||||
|
id: str
|
||||||
|
url: str
|
||||||
|
platform: str
|
||||||
|
name: str
|
||||||
|
author: str
|
||||||
|
category: str
|
||||||
|
description: str
|
||||||
|
difficulty: str
|
||||||
|
points: int
|
||||||
|
files: List[str]
|
||||||
|
additional_info: Dict = None
|
||||||
|
template: str = None
|
||||||
|
template_translated: str = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class File:
|
||||||
|
name: str
|
||||||
|
hash: str
|
||||||
|
url: str
|
||||||
34
src/platforms/base.py
Normal file
34
src/platforms/base.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
import requests
|
||||||
|
from http.cookiejar import CookieJar
|
||||||
|
from pathlib import Path
|
||||||
|
from ..models import Challenge
|
||||||
|
|
||||||
|
class CTFPlatform(ABC):
|
||||||
|
"""Abstract base class for CTF platforms"""
|
||||||
|
def __init__(self, url: str, cookies: Optional[CookieJar] = None):
|
||||||
|
self.base_url = url
|
||||||
|
self.session = requests.Session()
|
||||||
|
if cookies:
|
||||||
|
self.session.cookies = cookies
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def login(self, credentials: Dict) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_challenges(self) -> List[Challenge]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_challenge(self, challenge_url: str) -> Challenge:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def download_challenge_files(self, challenge: Challenge, destination: Path) -> List[Path]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate_template(self, challenge: Challenge, hugo_header: bool, translated: bool) -> str:
|
||||||
|
pass
|
||||||
267
src/platforms/hackropole.py
Normal file
267
src/platforms/hackropole.py
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
from typing import List, Dict
|
||||||
|
from pathlib import Path
|
||||||
|
import requests
|
||||||
|
from .base import CTFPlatform
|
||||||
|
from ..models import Challenge, File
|
||||||
|
from ..utils.config_handler import load_config
|
||||||
|
from ..utils.challenge_handler import download_files
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import unicodedata
|
||||||
|
from datetime import datetime
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
|
||||||
|
class HackropolePlatform(CTFPlatform):
|
||||||
|
def __init__(
|
||||||
|
self, url: str = "https://hackropole.fr", config_file: str | Path = None
|
||||||
|
):
|
||||||
|
super().__init__(url)
|
||||||
|
if config_file:
|
||||||
|
self.load_config(config_file)
|
||||||
|
self.headers = {
|
||||||
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||||
|
"accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
"pragma": "no-cache",
|
||||||
|
"priority": "u=0, i",
|
||||||
|
"referer": "https://hackropole.fr/",
|
||||||
|
"sec-ch-ua": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": '"macOS"',
|
||||||
|
"sec-fetch-dest": "document",
|
||||||
|
"sec-fetch-mode": "navigate",
|
||||||
|
"sec-fetch-site": "same-origin",
|
||||||
|
"sec-fetch-user": "?1",
|
||||||
|
"upgrade-insecure-requests": "1",
|
||||||
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_config(self, config_file: str | Path):
|
||||||
|
"""Load configuration from file"""
|
||||||
|
config = load_config(config_file)
|
||||||
|
self.token = config.get("token")
|
||||||
|
self.provider = config.get("provider")
|
||||||
|
|
||||||
|
def login(self) -> bool:
|
||||||
|
"""
|
||||||
|
Not implementing traditional login as we're using cookies
|
||||||
|
Returns True if we can access authenticated endpoints
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"token": self.token,
|
||||||
|
"provider": self.provider,
|
||||||
|
}
|
||||||
|
response = self.session.post(
|
||||||
|
"https://hackropole.fr/api/hackropole/user/self",
|
||||||
|
headers=self.headers,
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
return response.status_code == 200
|
||||||
|
except requests.RequestException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_challenges(self) -> List[Challenge]:
|
||||||
|
raise NotImplementedError("Method not implemented")
|
||||||
|
|
||||||
|
def get_challenge(self, challenge_url: str) -> Challenge:
|
||||||
|
"""Get a specific challenge by URL"""
|
||||||
|
try:
|
||||||
|
response = self.session.get(challenge_url, headers=self.headers)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception(
|
||||||
|
f"Error fetching challenge {challenge_url}: {response.status_code}"
|
||||||
|
)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise Exception(f"Error fetching challenge {challenge_url}: {e}")
|
||||||
|
|
||||||
|
response.encoding = "utf-8" # Force UTF-8 encoding
|
||||||
|
soup = BeautifulSoup(response.text, "html.parser")
|
||||||
|
|
||||||
|
title = unicodedata.normalize(
|
||||||
|
"NFKD", soup.select_one(".jumbotron h1").get_text(strip=True)
|
||||||
|
)
|
||||||
|
id = "-".join([word.lower() for word in title.split()])
|
||||||
|
|
||||||
|
badges = [
|
||||||
|
unicodedata.normalize("NFKD", badge.get_text(strip=True))
|
||||||
|
for badge in soup.select(".jumbotron .badge")
|
||||||
|
if "résolu le"
|
||||||
|
not in unicodedata.normalize("NFKD", badge.get_text(strip=True))
|
||||||
|
]
|
||||||
|
description = unicodedata.normalize(
|
||||||
|
"NFKD", soup.select_one(".markdown p").get_text(strip=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
file_links = []
|
||||||
|
file_names = []
|
||||||
|
file_hashes = {}
|
||||||
|
file_elements = soup.select(".list-file li")
|
||||||
|
for element in file_elements:
|
||||||
|
link = element.select_one("a")["href"]
|
||||||
|
name = (
|
||||||
|
element.select_one("a").get("download")
|
||||||
|
or element.select_one("a")["href"].split("/")[-1]
|
||||||
|
)
|
||||||
|
file_links.append(link)
|
||||||
|
file_names.append(name)
|
||||||
|
hash_element = element.select_one(".clip-sha256")
|
||||||
|
if hash_element:
|
||||||
|
hash_text = hash_element.get_text(strip=True)
|
||||||
|
file_hash = hash_text.split("–")[-1].strip()
|
||||||
|
file_hashes[name] = file_hash
|
||||||
|
|
||||||
|
author_name = unicodedata.normalize(
|
||||||
|
"NFKD",
|
||||||
|
soup.select_one(".col.text-center .font-monospace").get_text(strip=True),
|
||||||
|
)
|
||||||
|
author_avatar = soup.select_one(".col.text-center img")["src"]
|
||||||
|
|
||||||
|
stars = len(
|
||||||
|
[
|
||||||
|
star
|
||||||
|
for star in soup.select("svg.text-warning")
|
||||||
|
if star.select_one("title").get_text(strip=True) == "star"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
available_categories = [
|
||||||
|
"crypto",
|
||||||
|
"forensics",
|
||||||
|
"hardware",
|
||||||
|
"misc",
|
||||||
|
"pwn",
|
||||||
|
"reverse",
|
||||||
|
"web",
|
||||||
|
]
|
||||||
|
for badge in badges:
|
||||||
|
if badge.lower() in available_categories:
|
||||||
|
category = badge
|
||||||
|
break
|
||||||
|
|
||||||
|
files = [
|
||||||
|
File(name=file_name, hash=file_hashes.get(file_name), url=file_link)
|
||||||
|
for file_name, file_link in zip(file_names, file_links)
|
||||||
|
]
|
||||||
|
|
||||||
|
return Challenge(
|
||||||
|
id=id,
|
||||||
|
url=challenge_url,
|
||||||
|
platform="Hackropole",
|
||||||
|
name=title,
|
||||||
|
author=author_name,
|
||||||
|
category=category if category else "Uncategorized",
|
||||||
|
description=description,
|
||||||
|
difficulty=stars,
|
||||||
|
points=None,
|
||||||
|
files=files,
|
||||||
|
additional_info={"badges": badges, "author_avatar": author_avatar},
|
||||||
|
)
|
||||||
|
|
||||||
|
def download_challenge_files(self, challenge: Challenge, output_dir: Path):
|
||||||
|
download_files(self, challenge, output_dir)
|
||||||
|
|
||||||
|
def generate_template(self, challenge: Challenge, hugo_header: bool = False, translated: bool = False):
|
||||||
|
"""Generate writeup template for challenge"""
|
||||||
|
|
||||||
|
all_tags = list(set([challenge.category] + challenge.additional_info["badges"])) # Remove duplicates
|
||||||
|
tags_str = '", "'.join(all_tags)
|
||||||
|
|
||||||
|
hugo_header_template = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
---
|
||||||
|
title: "{challenge.name}"
|
||||||
|
date: "{datetime.now().isoformat()}"
|
||||||
|
tags: ["{tags_str}"]
|
||||||
|
author: "Noham"
|
||||||
|
summary: "Writeup for {challenge.name} from {challenge.platform}. A {challenge.category} challenge with a difficulty of {challenge.difficulty if challenge.difficulty > 0 else 1}/5."
|
||||||
|
showToc: false
|
||||||
|
TocOpen: false
|
||||||
|
draft: false
|
||||||
|
hidemeta: false
|
||||||
|
comments: true
|
||||||
|
disableHLJS: false
|
||||||
|
disableShare: false
|
||||||
|
hideSummary: false
|
||||||
|
searchHidden: false
|
||||||
|
ShowReadingTime: true
|
||||||
|
ShowBreadCrumbs: true
|
||||||
|
searchHidden: true
|
||||||
|
ShowPostNavLinks: true
|
||||||
|
ShowWordCount: true
|
||||||
|
ShowRssButtonInSectionTermList: true
|
||||||
|
UseHugoToc: true
|
||||||
|
---
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
hugo_header_template_fr = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
---
|
||||||
|
title: "{challenge.name}"
|
||||||
|
date: "{datetime.now().isoformat()}"
|
||||||
|
tags: ["{tags_str}"]
|
||||||
|
author: "Noham"
|
||||||
|
summary: "Writeup pour {challenge.name} de {challenge.platform}. Un challenge de {challenge.category} avec une difficulté de {challenge.difficulty if challenge.difficulty > 0 else 1}/5."
|
||||||
|
showToc: false
|
||||||
|
TocOpen: false
|
||||||
|
draft: false
|
||||||
|
hidemeta: false
|
||||||
|
comments: true
|
||||||
|
disableHLJS: false
|
||||||
|
disableShare: false
|
||||||
|
hideSummary: false
|
||||||
|
searchHidden: false
|
||||||
|
ShowReadingTime: true
|
||||||
|
ShowBreadCrumbs: true
|
||||||
|
searchHidden: true
|
||||||
|
ShowPostNavLinks: true
|
||||||
|
ShowWordCount: true
|
||||||
|
ShowRssButtonInSectionTermList: true
|
||||||
|
UseHugoToc: true
|
||||||
|
---
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
stars = "⭐" * (challenge.difficulty if challenge.difficulty > 0 else 1)
|
||||||
|
|
||||||
|
files_section = ""
|
||||||
|
for file in challenge.files:
|
||||||
|
hash_text = f" *(SHA256: {file.hash})*" if file.hash else ""
|
||||||
|
files_section += f"[{file.name}]({file.url}){hash_text}, "
|
||||||
|
files_section = files_section.rstrip(", ")
|
||||||
|
|
||||||
|
main_content = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
- Author: {challenge.author}
|
||||||
|
- Category: {challenge.category}
|
||||||
|
- Difficulty: {stars} ({challenge.difficulty if challenge.difficulty > 0 else 1}/5)
|
||||||
|
- Challenge URL: [{challenge.name} - {challenge.platform}]({challenge.url})
|
||||||
|
- Challenge description: {challenge.description}
|
||||||
|
- Files provided: {files_section}
|
||||||
|
|
||||||
|
## Writeup
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
main_content_fr = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
- Auteur: {challenge.author}
|
||||||
|
- Catégorie: {challenge.category}
|
||||||
|
- Difficulté: {stars} ({challenge.difficulty if challenge.difficulty > 0 else 1}/5)
|
||||||
|
- URL du challenge: [{challenge.name} - {challenge.platform}]({challenge.url})
|
||||||
|
- Description du challenge: {challenge.description}
|
||||||
|
- Fichiers fournis: {files_section}
|
||||||
|
|
||||||
|
## Writeup
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if hugo_header:
|
||||||
|
challenge.template = hugo_header_template + main_content
|
||||||
|
if translated:
|
||||||
|
challenge.template_translated = hugo_header_template_fr + main_content_fr
|
||||||
|
else:
|
||||||
|
challenge.template = main_content
|
||||||
|
if translated:
|
||||||
|
challenge.template_translated = main_content_fr
|
||||||
19
src/utils/challenge_handler.py
Normal file
19
src/utils/challenge_handler.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from ..models import Challenge, File
|
||||||
|
from typing import List
|
||||||
|
from pathlib import Path
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def download_files(self, challenge: Challenge, destination: Path) -> List[Path]:
|
||||||
|
"""Download challenge files to the specified directory"""
|
||||||
|
for file in challenge.files:
|
||||||
|
file_url = file.url
|
||||||
|
name = file.name
|
||||||
|
name = name.replace("public.yml", ".yml")
|
||||||
|
try:
|
||||||
|
response = self.session.get(file_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
file_path = destination / name
|
||||||
|
file_path.write_bytes(response.content)
|
||||||
|
print(f"Downloaded {name} to {destination}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise f"Error downloading file {file_url}: {e}"
|
||||||
18
src/utils/config_handler.py
Normal file
18
src/utils/config_handler.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import json
|
||||||
|
from typing import Dict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def load_config(file_path: str | Path) -> Dict:
|
||||||
|
"""Load configuration from a JSON file
|
||||||
|
Expected format:
|
||||||
|
{
|
||||||
|
"token": "your_token",
|
||||||
|
"provider": "provider_name"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
path = Path(file_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Config file not found: {file_path}")
|
||||||
|
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
19
src/utils/cookie_handler.py
Normal file
19
src/utils/cookie_handler.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import json
|
||||||
|
from typing import Dict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def load_cookies_from_file(file_path: str | Path) -> Dict:
|
||||||
|
"""
|
||||||
|
Load cookies from a JSON file.
|
||||||
|
Expected format:
|
||||||
|
{
|
||||||
|
"cookie_name1": "cookie_value1",
|
||||||
|
"cookie_name2": "cookie_value2"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
path = Path(file_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Cookie file not found: {file_path}")
|
||||||
|
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
Reference in New Issue
Block a user