mirror of https://github.com/dirtbags/moth.git
Merge pull request #78 from int00h5525/add_objectives_support
Add objectives support
This commit is contained in:
commit
e4e106a3ec
|
@ -9,7 +9,8 @@ RUN apk --no-cache add \
|
|||
&& \
|
||||
pip3 install \
|
||||
scapy==2.4.2 \
|
||||
pillow==5.4.1
|
||||
pillow==5.4.1 \
|
||||
PyYAML==5.1.1
|
||||
|
||||
|
||||
COPY devel /app/
|
||||
|
|
173
devel/moth.py
173
devel/moth.py
|
@ -13,6 +13,7 @@ import random
|
|||
import string
|
||||
import tempfile
|
||||
import shlex
|
||||
import yaml
|
||||
|
||||
messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
|
||||
|
@ -59,6 +60,34 @@ class PuzzleFile:
|
|||
self.name = name
|
||||
self.visible = visible
|
||||
|
||||
class PuzzleSuccess(dict):
|
||||
"""Puzzle success objectives
|
||||
|
||||
:param acceptable: Learning outcome from acceptable knowledge of the subject matter
|
||||
:param mastery: Learning outcome from mastery of the subject matter
|
||||
"""
|
||||
|
||||
valid_fields = ["acceptable", "mastery"]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(PuzzleSuccess, self).__init__()
|
||||
for key in self.valid_fields:
|
||||
self[key] = None
|
||||
for key, value in kwargs.items():
|
||||
if key in self.valid_fields:
|
||||
self[key] = value
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in self.valid_fields:
|
||||
return self[attr]
|
||||
raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, attr))
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr in self.valid_fields:
|
||||
self[attr] = value
|
||||
else:
|
||||
raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, attr))
|
||||
|
||||
|
||||
class Puzzle:
|
||||
def __init__(self, category_seed, points):
|
||||
|
@ -80,6 +109,13 @@ class Puzzle:
|
|||
self.hint = None
|
||||
self.files = {}
|
||||
self.body = io.StringIO()
|
||||
|
||||
# NIST NICE objective content
|
||||
self.objective = None # Text describing the expected learning outcome from solving this puzzle, *why* are you solving this puzzle
|
||||
self.success = PuzzleSuccess() # Text describing criteria for different levels of success, e.g. {"Acceptable": "Did OK", "Mastery": "Did even better"}
|
||||
self.solution = None # Text describing how to solve the puzzle
|
||||
self.ksas = [] # A list of references to related NICE KSAs (e.g. K0058, . . .)
|
||||
|
||||
self.logs = []
|
||||
self.randseed = category_seed * self.points
|
||||
self.rand = random.Random(self.randseed)
|
||||
|
@ -91,47 +127,102 @@ class Puzzle:
|
|||
|
||||
def read_stream(self, stream):
|
||||
header = True
|
||||
line = ""
|
||||
if stream.read(3) == "---":
|
||||
header = "yaml"
|
||||
else:
|
||||
header = "moth"
|
||||
|
||||
stream.seek(0)
|
||||
|
||||
if header == "yaml":
|
||||
self.read_yaml_header(stream)
|
||||
elif header == "moth":
|
||||
self.read_moth_header(stream)
|
||||
|
||||
for line in stream:
|
||||
if header:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
header = False
|
||||
continue
|
||||
key, val = line.split(':', 1)
|
||||
key = key.lower()
|
||||
val = val.strip()
|
||||
if key == 'author':
|
||||
self.authors.append(val)
|
||||
elif key == 'summary':
|
||||
self.summary = val
|
||||
elif key == 'answer':
|
||||
self.answers.append(val)
|
||||
elif key == 'pattern':
|
||||
self.pattern = val
|
||||
elif key == 'hint':
|
||||
self.hint = val
|
||||
elif key == 'name':
|
||||
pass
|
||||
elif key == 'file':
|
||||
parts = shlex.split(val)
|
||||
name = parts[0]
|
||||
hidden = False
|
||||
stream = open(name, 'rb')
|
||||
try:
|
||||
name = parts[1]
|
||||
hidden = (parts[2].lower() == "hidden")
|
||||
except IndexError:
|
||||
pass
|
||||
self.files[name] = PuzzleFile(stream, name, not hidden)
|
||||
elif key == 'script':
|
||||
stream = open(val, 'rb')
|
||||
# Make sure this shows up in the header block of the HTML output.
|
||||
self.files[val] = PuzzleFile(stream, val, visible=False)
|
||||
self.scripts.append(val)
|
||||
else:
|
||||
raise ValueError("Unrecognized header field: {}".format(key))
|
||||
self.body.write(line)
|
||||
|
||||
def read_yaml_header(self, stream):
|
||||
contents = ""
|
||||
header = False
|
||||
for line in stream:
|
||||
if line.strip() == "---" and header: # Handle last line
|
||||
break
|
||||
elif line.strip() == "---": # Handle first line
|
||||
header = True
|
||||
continue
|
||||
else:
|
||||
self.body.write(line)
|
||||
contents += line
|
||||
|
||||
config = yaml.safe_load(contents)
|
||||
for key, value in config.items():
|
||||
key = key.lower()
|
||||
self.handle_header_key(key, value)
|
||||
|
||||
def read_moth_header(self, stream):
|
||||
for line in stream:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
break
|
||||
|
||||
key, val = line.split(':', 1)
|
||||
key = key.lower()
|
||||
val = val.strip()
|
||||
self.handle_header_key(key, val)
|
||||
|
||||
def handle_header_key(self, key, val):
|
||||
if key == 'author':
|
||||
self.authors.append(val)
|
||||
elif key == 'summary':
|
||||
self.summary = val
|
||||
elif key == 'answer':
|
||||
if not isinstance(val, str):
|
||||
raise ValueError("Answers must be strings, got %s, instead" % (type(val),))
|
||||
self.answers.append(val)
|
||||
elif key == "answers":
|
||||
for answer in val:
|
||||
if not isinstance(answer, str):
|
||||
raise ValueError("Answers must be strings, got %s, instead" % (type(answer),))
|
||||
self.answers.append(answer)
|
||||
elif key == 'pattern':
|
||||
self.pattern = val
|
||||
elif key == 'hint':
|
||||
self.hint = val
|
||||
elif key == 'name':
|
||||
pass
|
||||
elif key == 'file':
|
||||
parts = shlex.split(val)
|
||||
name = parts[0]
|
||||
hidden = False
|
||||
stream = open(name, 'rb')
|
||||
try:
|
||||
name = parts[1]
|
||||
hidden = (parts[2].lower() == "hidden")
|
||||
except IndexError:
|
||||
pass
|
||||
self.files[name] = PuzzleFile(stream, name, not hidden)
|
||||
elif key == 'script':
|
||||
stream = open(val, 'rb')
|
||||
# Make sure this shows up in the header block of the HTML output.
|
||||
self.files[val] = PuzzleFile(stream, val, visible=False)
|
||||
self.scripts.append(val)
|
||||
elif key == "objective":
|
||||
self.objective = val
|
||||
elif key == "success":
|
||||
# Force success dictionary keys to be lower-case
|
||||
self.success = dict((x.lower(), y) for x,y in val.items())
|
||||
elif key == "success.acceptable":
|
||||
self.success.acceptable = val
|
||||
elif key == "success.mastery":
|
||||
self.success.mastery = val
|
||||
elif key == "solution":
|
||||
self.solution = val
|
||||
elif key == "ksa":
|
||||
self.ksas.append(val)
|
||||
else:
|
||||
raise ValueError("Unrecognized header field: {}".format(key))
|
||||
|
||||
|
||||
def read_directory(self, path):
|
||||
try:
|
||||
|
@ -278,6 +369,10 @@ class Puzzle:
|
|||
'scripts': self.scripts,
|
||||
'pattern': self.pattern,
|
||||
'body': self.html_body(),
|
||||
'objective': self.objective,
|
||||
'success': self.success,
|
||||
'solution': self.solution,
|
||||
'ksas': self.ksas,
|
||||
}
|
||||
|
||||
def hashes(self):
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
|
||||
import moth
|
||||
|
||||
|
@ -70,7 +71,9 @@ class MothValidator:
|
|||
def check_fields(self, puzzle):
|
||||
"""Check if the puzzle has the requested fields"""
|
||||
for field in self.required_fields:
|
||||
if not hasattr(puzzle, field):
|
||||
if not hasattr(puzzle, field) or \
|
||||
getattr(puzzle,field) is None or \
|
||||
getattr(puzzle,field) == "":
|
||||
raise MothValidationError("Missing field %s" % (field,))
|
||||
|
||||
@staticmethod
|
||||
|
@ -132,14 +135,34 @@ class MothValidator:
|
|||
|
||||
puzzle.body.seek(old_pos)
|
||||
|
||||
# Leaving this as a placeholder until KSAs are formally supported
|
||||
@staticmethod
|
||||
def check_ksa_format(puzzle):
|
||||
"""Check if KSAs are properly formatted"""
|
||||
|
||||
ksa_re = re.compile("^[KSA]\d{4}$")
|
||||
|
||||
if hasattr(puzzle, "ksa"):
|
||||
for ksa in puzzle.ksa:
|
||||
if not ksa.startswith("K"):
|
||||
raise MothValidationError("Unrecognized KSA format")
|
||||
if ksa_re.match(ksa) is None:
|
||||
raise MothValidationError("Unrecognized KSA format (%s)" % (ksa,))
|
||||
|
||||
@staticmethod
|
||||
def check_success(puzzle):
|
||||
"""Check if success criteria are defined"""
|
||||
|
||||
if not hasattr(puzzle, "success"):
|
||||
raise MothValidationError("Success not defined")
|
||||
|
||||
criteria = ["acceptable", "mastery"]
|
||||
missing_criteria = []
|
||||
for criterion in criteria:
|
||||
if criterion not in puzzle.success.keys() or \
|
||||
puzzle.success[criterion] is None or \
|
||||
len(puzzle.success[criterion]) == 0:
|
||||
missing_criteria.append(criterion)
|
||||
|
||||
if len(missing_criteria) > 0:
|
||||
raise MothValidationError("Missing success criteria (%s)" % (", ".join(missing_criteria)))
|
||||
|
||||
|
||||
def output_json(data):
|
||||
|
|
Loading…
Reference in New Issue