This blog post talks about CSIT Dec 25 Mini Challenge.
Introduction
Merry Christmas everyone! This month’s challenge is titled “Cloud Development Mini-Challenge”. Its a 3-part challenge hosted on killaconda that is a mix of python scripting and learning ansible! For those not too familiar with ansible, I know it as an automation engine for stuff like configuration management and pushing down applications / logging requirements to multiple hosts.

Walkthrough
Challenge 1
This challenge is a simple logical code amendment. They provided another text file which you will have to append to the back of the original inventory.ini file. Once thats done, just follow the instructions!

I wrote some quick comments in the raw code (playbook.yml) file below.
ubuntu:~/mini-challenge/tasks/t1$ cat playbook.yml
---
- name: Task 1 Playbook
hosts: localhost
gather_facts: false
vars:
nice_list: []
naughty_list: []
tasks:
# FIX ME - Edit the operators ">" and " 5 else nice_list }}"
naughty_list: "{{ naughty_list + [item] if hostvars[item]['score'] | int -
{{
nice_list[:5]
| map('regex_search', '.$')
| list
}}
- name: Get last letters of last 4 naughty children
set_fact:
naughty_last_letters_last4: >-
{{
naughty_list[-4:]
| map('regex_search', '.$')
| list
}}
- name: Show extracted letters
debug:
msg:
- "Last letters of first 5 nice children: {{ nice_last_letters_first5 }}"
- "Last letters of last 4 naughty children: {{ naughty_last_letters_last4 }}"
- name: Combine letters to form flag
set_fact:
flag: "{{ (nice_last_letters_first5 + naughty_last_letters_last4) | join('') }}"
- name: Show flag
debug:
msg: "FLAG{{'{'}}{{ flag }}{{'}'}}"
- name: Write output to a File
copy:
content: "{{ flag }}"
dest: "flag1.txt"
mode: '0644'
force: true
Executing this playbook will return the flag. Note that the killconda environment has a timeout of like an hour for free users, so use the echo command below if the instance restarts to quickly move to stage 2. Repeat this trick for stage 2 as well to quickly get to stage 3.

echo "merryxmas" > flag1.txt
Challenge 2
Now this is where you can see why Ansible is sooo useful when it comes to tasks that are repetitive. Challenge 2 requires us to “SSH” into every host machine and read a file locations in /var/xmas/santa_git.txt. You could technically do it manually and update the inventory.ini… but that will take ages. So lets use Ansible to help out.

I’ve attached my playbook.yml code below, but lets try to understand whats happening. The first task was to read the git file from the file path provided in the “raw” variable and then registering it as variable name “gift_file”. I believe ansible interprets this and will loop through the inventory.ini for every “anisible_host” ip and perform this command. The next block is to assign this variable as “gift” after trimming the line break. The next two chunks are provided in the script for properly format the text in a way the task checker is able to understand. From the “Collect all gifts” segment, the following code blocks are to identify the “unique” gift among all the ansible hosts. You could skip this step and manually eyeball it, or write another python script to regex out the gift and count the number of occurance for each gift. The unique gift should only have 1 count.
ubuntu:~/mini-challenge/tasks/t2$ cat playbook.yml
---
- name: Task 2 Playbook
hosts: Wishlist
gather_facts: false
vars:
inventory_src: "inventory.ini"
inventory_dst: "newInventory.ini"
tasks:
- name: Read gift file from file path
raw: cat /var/xmas/santa_gift.txt
register: gift_file
- name: Assign gift to host variable
set_fact:
gift: "{{ gift_file.stdout | trim }}"
- name: Generate updated inventory content (one line per host)
delegate_to:
run_once: true
set_fact:
# IMPT !!! Do not change anything in the stated block (Make your playbook work with this block). Else it would fail the validation.
updated_inventory: |
[Wishlist]
{% for host in groups['Wishlist'] %}
{{ "%-10s" | format(host) }} ansible_host={{ "%-15s" | format(hostvars[host].ansible_host) }} score={{ "%-3s" | format(hostvars[host].score) }} delivery_address={{ "%-17s" | format(hostvars[host].delivery_address) }} gift="{{ hostvars[host].gift }}"
{% endfor %}
# End of Block
- name: Write new inventory file locally
delegate_to: localhost
copy:
content: "{{ updated_inventory }}"
dest: "{{ inventory_dst }}"
mode: '0644'
- name: Collect all gifts
delegate_to: localhost
run_once: true
set_fact:
all_gifts: "{{ groups['Wishlist'] | map('extract', hostvars, 'gift') | list }}"
- name: Count gift occurrences
delegate_to: localhost
run_once: true
set_fact:
gift_counts: "{{ gift_counts | default({}) | combine({ item: (gift_counts[item] | default(0) + 1) }) }}"
loop: "{{ all_gifts }}"
- name: Determine most unique gift
delegate_to: localhost
run_once: true
set_fact:
unique_gift: "{{ (gift_counts | dict2items | sort(attribute='value') | first).key }}"
- name: Write flag2.txt
delegate_to: localhost
copy:
content: "{{ unique_gift }}"
dest: "flag2.txt"
mode: '0644'
Executing the playbook will yield the flag - “A Full Time Job At CSIT”. Hahaha what a fitting choice.

Challenge 3
Challenge 3 is be far the hardest of the 3-part challenge. It requires you to edit 4 files, the custom_filters.py, two main.yml for each of the two tasks and one final playobok.yml to pull everything together. I don’t have a screenshot for this as its way too lengthy, but heres the code for the three parts.
custom_filters.py
This is a fairly straight forward task, you have to create a python script that reads the raw data - inventory.ini and return it in a dictonary form that follows task 1’s requirements.
ubuntu:~/mini-challenge/tasks/t3/filter_plugins$ cat custom_filters.py
class FilterModule(object):
def filters(self):
return {
'sorted_delivery_addresses_dict': self.sorted_delivery_addresses_dict
}
def sorted_delivery_addresses_dict(self, hostvars):
"""
Returns a dictionary of delivery addresses sorted by last octet.
Key: delivery address
Value: dict with hostname, gift, and nice/naughty status
"""
deliveries = []
# Build intermediate list
for host, vars in hostvars.items():
addr = vars.get('delivery_address')
if not addr:
continue
gift = vars.get('gift', 'Unknown')
score = int(vars.get('score', 0))
status = 'nice' if score > 5 else 'naughty'
deliveries.append({
'address': addr,
'hostname': host,
'gift': gift,
'status': status
})
# Sort by last octet of IP address
deliveries.sort(key=lambda x: int(x['address'].split('.')[-1]))
# Convert to required dictionary format
sorted_dict = {}
for item in deliveries:
sorted_dict[item['address']] = {
'hostname': item['hostname'],
'gift': item['gift'],
'status': item['status']
}
return sorted_dict
If you want to test whether or not your python script is working as intended, you could run this test.yml.
- name: Test sorted_delivery_addresses_dict filter
hosts: localhost
gather_facts: false
tasks:
- name: Show sorted delivery dictionary
debug:
var: hostvars | sorted_delivery_addresses_dict
# this is the command to run the script.
ansible-playbook -i inventory.ini test.yml
main.yml
This next segment is the “include_role” feature - more information here 🔗. To be honest, its the first time I’m using Ansible too, I have no idea what this feature is used for. The key thing to look out for here is just renaming the variables properly.
ubuntu:~/mini-challenge/tasks/t3/roles/rudolph/tasks$ cat main.yml
#Do not change this block
- name: Pick a random number of attempts
set_fact:
attempts: "{{ 1 + (max_attempts | int | random(seed=target_ip)) }}"
#end of block
# Change what is needed.
- name: Simulate delivery attempts
set_fact:
nice_delivery_logs: >-
{{
nice_delivery_logs +
[ "Rudolph delivering gift " ~ target_gift ~
" to " ~ target_hostname ~
" at " ~ target_ip ~
" attempt " ~ ((i -
{{
naughty_delivery_logs +
[ "Dasher delivering gift " ~ target_gift ~
" to " ~ target_hostname ~
" at " ~ target_ip ~
" attempt " ~ ((i Now we got to put together the core script that calls all the functions. Summarized by ChatGPT - This Ansible playbook runs locally to build and execute a gift delivery plan by first transforming and sorting host data into a structured delivery_plan using a custom filter, then iterating over each destination to conditionally include either the rudolph role for “nice” recipients or the dasher role for everyone else, passing along the target’s hostname, IP address, and gift as variables. As the roles run, they are expected to append messages to shared in-memory lists tracking successful (nice) and unsuccessful or alternate (naughty) deliveries, and at the end of the playbook those accumulated logs are written to separate text files (nice_delivery_logs.txt and naughty_delivery_logs.txt) on the local machine.
```bash
ubuntu:~/mini-challenge/tasks/t3$ cat playbook.yml
---
- name: Task 3 Playbook
hosts: localhost
gather_facts: false
# Do not change the vars
vars:
nice_delivery_logs: []
naughty_delivery_logs: []
max_attempts: 3
tasks:
#Change what is needed
- name: Build sorted delivery plan
set_fact:
delivery_plan: "{{ hostvars | sorted_delivery_addresses_dict }}"
delegate_to: localhost
run_once: true
- name: Show delivery plan (optional debug)
debug:
var: delivery_plan
- name: Loop through sorted addresses and include appropriate role
include_role:
name: "{{ 'rudolph' if item.value.status == 'nice' else 'dasher' }}"
vars:
target_hostname: "{{ item.value.hostname }}"
target_ip: "{{ item.key }}"
target_gift: "{{ item.value.gift }}"
loop: "{{ delivery_plan | dict2items }}"
loop_control:
label: "{{ item.value.hostname }}"
- name: Write nice text
delegate_to: localhost
copy:
content: "{{ nice_delivery_logs | join('\n') }}"
dest: "nice_delivery_logs.txt"
mode: '0644'
- name: Write naughty text
delegate_to: localhost
copy:
content: "{{ naughty_delivery_logs | join('\n') }}"
dest: "naughty_delivery_logs.txt"
mode: '0644'

The final flag is the concatenation of the hash of both logger files. The final flag is: a0d35cc39b9a8d8a736585686b988c9f5211ecab9def10447de98acd49a79d0c. That concludes the final CSIT mini challenge of 2025! I hope everyone had as much fun as I had trying to solve these puzzles.
Cheers.