├── requirements.txt
├── .gitignore
├── .github
├── assets
│ ├── logos
│ │ ├── mcpb.png
│ │ └── malscript_logo.png
│ └── examples
│ │ ├── help_menu.png
│ │ ├── single-recipe.png
│ │ ├── download_recipes.png
│ │ └── recipe_chain_saved.png
└── docs
│ └── malscript_docs.md
├── malcore_playbook
├── __init__.py
├── lib
│ ├── __init__.py
│ ├── api.py
│ ├── cli.py
│ └── settings.py
├── main
│ ├── __init__.py
│ └── entry.py
├── execution
│ ├── __init__.py
│ └── recipe_exec.py
├── malscript
│ ├── __init__.py
│ └── parse.py
├── writers
│ ├── __init__.py
│ ├── pdf_output.py
│ ├── txt_output.py
│ ├── json_output.py
│ └── console_output.py
├── cli_tool
│ └── __init__.py
└── __version__.py
├── NOTICE.md
├── setup.py
├── LICENSE.md
└── README.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | test*
3 | config.json
4 | dist/*
5 | build/*
6 | malcore_playbook.egg-info/*
7 | *.pyc
--------------------------------------------------------------------------------
/.github/assets/logos/mcpb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PenetrumLLC/Malcore-Playbook/HEAD/.github/assets/logos/mcpb.png
--------------------------------------------------------------------------------
/.github/assets/examples/help_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PenetrumLLC/Malcore-Playbook/HEAD/.github/assets/examples/help_menu.png
--------------------------------------------------------------------------------
/.github/assets/logos/malscript_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PenetrumLLC/Malcore-Playbook/HEAD/.github/assets/logos/malscript_logo.png
--------------------------------------------------------------------------------
/.github/assets/examples/single-recipe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PenetrumLLC/Malcore-Playbook/HEAD/.github/assets/examples/single-recipe.png
--------------------------------------------------------------------------------
/.github/assets/examples/download_recipes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PenetrumLLC/Malcore-Playbook/HEAD/.github/assets/examples/download_recipes.png
--------------------------------------------------------------------------------
/.github/assets/examples/recipe_chain_saved.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PenetrumLLC/Malcore-Playbook/HEAD/.github/assets/examples/recipe_chain_saved.png
--------------------------------------------------------------------------------
/malcore_playbook/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 |
--------------------------------------------------------------------------------
/malcore_playbook/lib/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 |
--------------------------------------------------------------------------------
/malcore_playbook/main/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 |
--------------------------------------------------------------------------------
/malcore_playbook/execution/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 |
--------------------------------------------------------------------------------
/malcore_playbook/malscript/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 |
--------------------------------------------------------------------------------
/malcore_playbook/writers/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 |
--------------------------------------------------------------------------------
/malcore_playbook/writers/pdf_output.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 | def output(out, filename):
8 | raise NotImplementedError("PDF output is not implemented yet")
9 |
--------------------------------------------------------------------------------
/malcore_playbook/cli_tool/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 | import malcore_playbook.main.entry as entry
8 |
9 |
10 | def run():
11 | """ CLI tool execution entrypoint """
12 | entry.main()
13 |
--------------------------------------------------------------------------------
/malcore_playbook/writers/txt_output.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 | import json
8 |
9 |
10 | def output(out, filename):
11 | with open(filename, "w") as fh:
12 | fh.write(str(out))
13 | return filename
14 |
--------------------------------------------------------------------------------
/malcore_playbook/__version__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 1:06 PM
4 | #
5 | #
6 |
7 | # major.minor.patch.commit
8 | VERSION = "1.0.4.5"
9 | # alias for the version
10 | VERSION_ALIAS = "storm"
11 | # version string
12 | VERSION_STRING = f"{VERSION}({VERSION_ALIAS})"
13 |
--------------------------------------------------------------------------------
/malcore_playbook/writers/json_output.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 12:59 PM
4 | #
5 | #
6 |
7 | import json
8 |
9 |
10 | def output(out, filename):
11 | if not isinstance(out, dict):
12 | out = {"output": out}
13 | with open(filename, 'w') as fh:
14 | json.dump(out, fh, indent=4)
15 | return filename
16 |
--------------------------------------------------------------------------------
/malcore_playbook/writers/console_output.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 |
7 | import json
8 |
9 |
10 | def output(out, filename):
11 | if isinstance(out, str):
12 | return f"\n{'*' * 63}\n{out}\n{'*' * 63}"
13 | else:
14 | return f"\n{'*' * 63}\n{json.dumps(out['data'], indent=2)}\n{'*' * 63}"
15 |
16 |
--------------------------------------------------------------------------------
/NOTICE.md:
--------------------------------------------------------------------------------
1 | # NOTICE
2 | ### Malcore Playbook Licensing Notice
3 |
4 | This software is licensed under a dual-licensing model:
5 |
6 | #### Non-Commercial Use:
7 | ```
8 | You are free to use, modify, and distribute Malcore Playbook under the terms of the MIT License for personal,
9 | educational, and research purposes only.
10 | ```
11 |
12 | #### Commercial Use:
13 | ```
14 | Any commercial use, including but not limited to use by corporations, governments, or other organizations for profit or
15 | in production environments, requires the purchase of a commercial license.
16 | ```
17 |
18 | The full Terms of Use, including commercial licensing details, are available at: https://malcore.io/terms-of-use
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | from malcore_playbook import __version__
3 |
4 |
5 | if __name__ == "__main__":
6 | setup(
7 | name='malcore-playbook',
8 | packages=find_packages(),
9 | version=__version__.VERSION,
10 | description='Malcore Playbook automates malware analysis, malware triaging, '
11 | 'and analyst workflows using modular recipes and DSL scripting',
12 | author="Thomas Perkins",
13 | author_email="penetrumcorp@gmail.com",
14 | install_requires=["requests"],
15 | long_description=open("README.md").read(),
16 | long_description_content_type="text/markdown",
17 | url="https://github.com/PenetrumLLC/malcore-playbook",
18 | entry_points={
19 | 'console_scripts': [
20 | 'malcore-playbook=malcore_playbook.cli_tool:run',
21 | 'mcpb=malcore_playbook.cli_tool:run',
22 | ]
23 | }
24 | )
25 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ### Malcore Playbook is dual licensed please see [NOTICE.md](NOTICE.md) for more information
2 |
3 | Non-Commercial Use:
4 | ```txt
5 | Copyright 2025 Penetrum LLC
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
8 | documentation files (the “Software”), to deal in the Software without restriction, including without limitation
9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
10 | to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions
13 | of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
16 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 | ```
20 |
21 | Commercial Use:
22 | ```txt
23 | Any commercial use, including but not limited to use by corporations, governments, or other organizations for profit
24 | or in production environments, requires the purchase of a commercial license.
25 | ```
26 |
--------------------------------------------------------------------------------
/malcore_playbook/lib/api.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:30 AM
4 | #
5 | #
6 |
7 | import malcore_playbook.lib.settings as settings
8 |
9 | import requests
10 |
11 |
12 | class Api(object):
13 |
14 | """ malcore API object """
15 |
16 | def __init__(self, only_remote=False):
17 | self.api_url = "https://api.malcore.io/api"
18 | self.auth_url = "https://api.malcore.io/auth"
19 | self.plan_url = "https://api.malcore.io/plan"
20 | if not only_remote:
21 | self.conf = settings.load_conf()
22 | else:
23 | self.conf = {}
24 |
25 | def upload_file(self, filename, endpoint):
26 | """ uploada file to an endpoint """
27 | url = f"{self.api_url}/{endpoint}"
28 | files = {'filename1': open(filename, 'rb')}
29 | headers = {'apiKey': self.conf['api_key']}
30 | req = requests.post(url, files=files, headers=headers)
31 | try:
32 | return req.json()
33 | except:
34 | return None
35 |
36 | def login(self, username, password):
37 | """ login to the API to get the user API key and auth tokens """
38 | post_data = {"email": username, "password": password}
39 | url = f"{self.auth_url}/login"
40 | try:
41 | req = requests.post(url, data=post_data)
42 | results = req.json()
43 | except:
44 | results = None
45 | return results
46 |
47 | def list_recipes(self):
48 | """ gather a list of all available recipes """
49 | url = "https://recipes.malcore.io/assets/dbs/files.json"
50 | req = requests.get(url)
51 | data = req.json()
52 | return data
53 |
54 | def status_check(self, uuid):
55 | """ check the status of executable file analysis """
56 | url = f"{self.api_url}/status"
57 | data = {"uuid": uuid}
58 | headers = {'apiKey': self.conf['api_key']}
59 | req = requests.post(url, data=data, headers=headers)
60 | return req.json()
61 |
--------------------------------------------------------------------------------
/malcore_playbook/execution/recipe_exec.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:30 AM
4 | #
5 | #
6 |
7 | import os
8 | import sys
9 |
10 | import malcore_playbook.lib.settings as settings
11 |
12 |
13 | def load_recipe(recipes, load_one=False, speak=True):
14 | """ import the loaded recipes """
15 | loaded = []
16 | for recipe in recipes:
17 | if speak:
18 | try:
19 | settings.logger.info(f"Attempting to load recipe: {recipe.__name__}")
20 | except:
21 | settings.logger.info("Attempting to load recipe")
22 | recipe_home = settings.RECIPE_HOME
23 | full_path = os.path.join(recipe_home, recipe)
24 | path, fname = os.path.split(full_path)
25 | modulename, _ = os.path.splitext(fname)
26 | if path not in sys.path:
27 | if speak:
28 | settings.logger.debug("Path not found in sys.path adding to it")
29 | sys.path.insert(0, path)
30 | try:
31 | imported_mod = __import__(modulename)
32 | if settings.user_can_use_recipe(imported_mod.__excluded_plans__):
33 | loaded.append(__import__(modulename))
34 | else:
35 | if speak:
36 | settings.logger.warning(
37 | f"Your plan does not allow usage of recipe: {recipe}, "
38 | f"to upgrade your plan see here: https://malcore.io/pricing"
39 | )
40 | except Exception as e:
41 | if speak:
42 | settings.logger.error(f"Cannot import recipe: {recipe}, hit error: {str(e)}")
43 | if len(loaded) != 0:
44 | if load_one:
45 | return loaded[0]
46 | else:
47 | return loaded
48 | else:
49 | settings.logger.warning("No recipes loaded successfully")
50 | return None
51 |
52 |
53 | def execute_recipe(recipe, *args, **kwargs):
54 | """ execute the loaded recipes and start processing them """
55 | settings.logger.info(f"Attempting to execute recipe: {recipe}")
56 | try:
57 | results = recipe.plugin(*args, **kwargs)
58 | if results is not None:
59 | settings.logger.debug(f"Recipe executed successfully returning results")
60 | return results
61 | except Exception as e:
62 | settings.logger.error(f"Unable to execute recipe, got error: {str(e)}")
63 | return None
64 |
--------------------------------------------------------------------------------
/.github/docs/malscript_docs.md:
--------------------------------------------------------------------------------
1 | Language type breakdown:
2 |
3 | | Feature | Classification |
4 | |-------------------------|---------------------------------------------------------------------------|
5 | | Purpose-built | DSL tailored for analysis, workflow automation, and automatic triaging |
6 | | Imperative flow control | Condition logic resembles traditional scripting |
7 | | Declarative intent | Each line is designed to express what to do based on prior outputs |
8 | | Functional style | Uses built-in functions with little not no side effects or mutable states |
9 | | Minimal syntax | Provides a compact and expressive styling |
10 | | Dot-path dereferencing | Provides the ability to access JSON-like objects with `.` |
11 |
12 | ### Built ins
13 |
14 | - `$`
15 | - Use this to set a variable for future use: `$emu`
16 | - `=`
17 | - Use in conjunction with the variable set to set the variable to the action: `$emu=ACTION`
18 | - `str()`
19 | - Use to set a string for searching in condition statements: `str('exe')`
20 | - `int()`
21 | - Use to set an integer for searching in condition statements: `int(182)`
22 | - `exec()`
23 | - Use to perform execution of a downloaded recipe, must be used with variable setting: `$emu=exec(dynamic-analysis)`
24 | - `if`
25 | - Use to perform a condition statement: `if str('exe')`
26 | - `ret`
27 | - Use to return a value from the script execution: `ret($emu)`
28 | - `.`
29 | - Use to access data within a variable: `$emu.data.[1]`
30 | - `;`
31 | - Use to end a line, must be at the end of every line: `$emu=exec(dynamic-analysis);`
32 |
33 | ### Setting and Accessing Variables
34 |
35 | To set a variable in MalScript you will need declare the variable and set it to an action, for example: `$emu=exec(dynamic-analysis);` will set the variable `$emu` to the output of the `dynamic-analysis` recipe. You will then be able to access the output from the recipe by calling the set variable.
36 |
37 | Since the recipes are based off a RESTful API on the Malcore website you are also able to access variables and list indexes within the output by chaining it with `.` (periods) to the location of the required data. For example if we have the following assigned to variable `$results`:
38 |
39 | ```json
40 | {
41 | "results": {
42 | "test1": [
43 | {"test2": "results"}
44 | ]
45 | }
46 | }
47 | ```
48 |
49 | We can access the `results` variable by chaining the location together: `$results.results.test1.[0].test2` will give us the string `"results"`.
50 |
51 | ### Executing Recipes and Assigning them to Variables
52 |
53 | The `exec()` built in allows you to execute a recipe. For example: `$exifData=exec(exif-data);`. To break this down:
54 |
55 | ```
56 | $VAR_NAME # set the variable name
57 | = # Assign the variable
58 | exec( # Execute a recipe
59 | RECIPE-NAME # Execute this recipe name
60 | ) # Close the brackets
61 | ; # End the line
62 | ```
63 |
64 | ### Example Scripts
65 |
66 | #### Functionality
67 |
68 | This is a basic overview of MalScript that provides decent understanding of functionality
69 |
70 | ```bash
71 | # This is a comment that must also end with: ;
72 | # We will set the variable $s to the output of executing the strings recipe ;
73 | $s=exec(strings);
74 | # If we find a string matching GetCurrentProcess in the strings output ;
75 | # We execute the threat-score recipe and assign the output to variable $t ;
76 | if str('GetCurrentProcess') in $s then $t=exec(threat-score);
77 | # We can access the exact score variable by using derreferencing with . ;
78 | # If 5.13 is in the JSON key score we will execute the AI classifier recipe ;
79 | if str('5.13') in $t.score then $a=exec(ai-class);
80 | # If the string safe is in the AI classifier output ;
81 | # We will execute the exif-data recipe and assign output to variable $e ;
82 | if str('safe') in $a then $e=exec(exif-data);
83 | # Then we will return the code_signature JSON variable from the exif-data output ;
84 | ret($e.code_signature);
85 | ```
86 |
87 | #### Check Code Signing
88 |
89 | This is an example of checking if a script has a signing certificate in it
90 |
91 | ```bash
92 | # We will first create an execution for exif-data and put the results in the $exif variable ;
93 | $exif=exec(exif-data);
94 | # If we find the string 'Microsoft' in the $exif.signature_info output classify the file ;
95 | if str('Microsoft') in $exif.signature_info then $ai=exec(ai-class);
96 | # Return the classification
97 | ret($ai);
98 | ```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Malcore Playbook
2 |
3 |

4 |
5 | [](https://recipes.malcore.io)
6 | [](https://github.com/PenetrumLLC/Malcore-Playbook/blob/master/.github/docs/malscript_docs.md)
7 | [](https://github.com/PenetrumLLC/Malcore-Playbook-Recipes)
8 |
9 | Malcore Playbook is a powerful framework for automating malware analysis, malware triaging, and analyst workflows using modular recipes and scripting. Designed for SOC analysts, threat hunters, and cybersecurity professionals, Malcore Playbook allows users to build chains to automate workflows, and extract actionable intelligence from suspicious files through a simple, flexible scripting language, and individual recipes.
10 |
11 | ## Installation
12 |
13 | It is advised to use the this method for installation:
14 |
15 | ```shell
16 | pip install malcore-playbook
17 | ```
18 |
19 | Then you can easily update by running: `pip install malcore-playbook --upgrade`
20 |
21 | ---
22 |
23 | You are also able to install manually like so:
24 |
25 | ```shell
26 | git clone https://github.com/PenetrumLLC/Malcore-Playbook.git && \
27 | cd Malcore-Playbook && \
28 | python setup.py install && \
29 | malcore-playbook
30 | ```
31 |
32 | ## Usage
33 |
34 | ```
35 | usage: malcore-playbook --recipe RECIPE[,RECIPE,..] --filename FILE [--chain --script [SCRIPT]
36 | --kwargs ARG1=VAL1[,ARG2=VAL2,...]]
37 |
38 | optional arguments:
39 | -h, --help show this help message and exit
40 |
41 | required arguments:
42 | -r RECIPE-NAME [RECIPE-NAME ...], --recipe RECIPE-NAME [RECIPE-NAME ...]
43 | Recipes to execute one at a time, pass multiple using a comma seperated list
44 | (eg, recipe1,recipe2,...)
45 | -c, --chain Pass this to chain recipes together with a script, must pass the --script flag with this
46 | --filename FILENAME, -f FILENAME, --file-to-analyze FILENAME
47 | The filename that you want to process with the recipes. This is required for the recipes to work
48 |
49 | chain related arguments:
50 | --chain-script CHAIN-SCRIPT, -S CHAIN-SCRIPT, --script CHAIN-SCRIPT, -C CHAIN-SCRIPT
51 | Pass either a filename or a raw chain script in order to execute the MalScript chain
52 |
53 | recipe related arguments:
54 | --list-remote, --list-remote-recipes, -lR
55 | List all remote recipes that are available for download
56 | --list-local, --list-local-recipes, -lL
57 | List all local recipes that are available to execute
58 | --download-remote RECIPE-NAME [RECIPE-NAME ...], --download-recipe RECIPE-NAME [RECIPE-NAME ...],
59 | --download RECIPE-NAME [RECIPE-NAME ...], -D RECIPE-NAME [RECIPE-NAME ...]
60 | Pass a remote recipe name to download it to your recipe folder
61 | (pass 'all' to download all available recipes)
62 | --recipe-updates ACTION
63 | Check for recipe updates
64 | --kwargs [KWARGS [KWARGS ...]]
65 | Key and value pairs to pass to the recipe IE: arg1=var1,arg2=var2
66 |
67 | misc arguments:
68 | --force Force actions that would otherwise fail
69 | --output OUTPUT-TYPE, -O OUTPUT-TYPE, --output-type OUTPUT-TYPE
70 | Pass to control the type of output you want, default is JSON files stored in: C:\Users\saman\.mcpb
71 | --hide Hide the banner
72 | --version Show version numbers and exit
73 | ```
74 |
75 | ## MalScript Overview
76 |
77 | 
78 |
79 | MalScript is a domain-specific scripting language (DSL) built specifically for the Malcore Playbook. This language is designed to automate malware analysis and file triaging workflows. By providing the ability to chain recipes and execute them conditionally, MalScript provides a powerful declarative automation to help automate reverse engineers and analysts. MalScript combines function and imperative elements to support rule-based execution, and data inspection on real-time analysis results.
80 |
81 | Full language documentation can be found [HERE](.github/docs/malscript_docs.md)
82 |
83 | ## Example Usage
84 |
85 | The help menu:
86 | 
87 |
88 | Executing a single recipe
89 | 
90 |
91 | Downloading recipes:
92 | 
93 |
94 |
95 | Executing a recipe chain and saving it to a text file:
96 | 
--------------------------------------------------------------------------------
/malcore_playbook/lib/cli.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:30 AM
4 | #
5 | #
6 |
7 | import sys
8 | import argparse
9 |
10 | import malcore_playbook.lib.settings as settings
11 |
12 |
13 | logger = settings.logger
14 |
15 |
16 | class Parser(argparse.ArgumentParser):
17 |
18 | """ class placeholder for the argparse item """
19 |
20 | @staticmethod
21 | def optparse():
22 | """ makes it easier to call from Parser().optparse() and looks nicer """
23 | parser = argparse.ArgumentParser()
24 |
25 | parser.usage = (f"malcore-playbook --recipe RECIPE[,RECIPE,..] --filename FILE "
26 | "[--chain --script [SCRIPT] "
27 | "--kwargs ARG1=VAL1[,ARG2=VAL2,...]]")
28 |
29 | required = parser.add_argument_group("required arguments")
30 | required.add_argument(
31 | "-r", "--recipe", nargs="+", metavar="RECIPE-NAME",
32 | help="Recipes to execute one at a time, pass multiple using a comma seperated list ("
33 | "eg, recipe1,recipe2,...)",
34 | default=None, dest="useRecipe"
35 | )
36 | required.add_argument(
37 | "-c", "--chain", action="store_true",
38 | dest="useChain", default=False,
39 | help="Pass this to chain recipes together with a script, must pass the --script flag with this"
40 | )
41 | required.add_argument(
42 | "--filename", "-f", "--file-to-analyze", nargs=1, default=None,
43 | help="The filename that you want to process with the recipes. This is required for the recipes to work",
44 | dest="filename"
45 | )
46 |
47 | chain_flags = parser.add_argument_group("chain related arguments")
48 | chain_flags.add_argument(
49 | "--chain-script", "-s", "--script", "-C", metavar="CHAIN-SCRIPT",
50 | dest="chainScript", default=None,
51 | help="Pass either a filename or a raw chain script in order to execute the MalScript chain"
52 | )
53 |
54 | recipe_args = parser.add_argument_group("recipe related arguments")
55 | recipe_args.add_argument(
56 | "--list-remote", "--list-remote-recipes", "-lR", action="store_true", default=False,
57 | help="List all remote recipes that are available for download", dest="viewRemote"
58 | )
59 | recipe_args.add_argument(
60 | "--list-local", "--list-local-recipes", "-lL", action="store_true", default=False,
61 | help="List all local recipes that are available to execute", dest="viewLocal"
62 | )
63 | recipe_args.add_argument(
64 | "--download-remote", "--download-recipe", "--download", "-D",
65 | nargs="+", metavar="RECIPE-NAME", default=None,
66 | help="Pass a remote recipe name to download it to your recipe folder ("
67 | "pass 'all' to download all available recipes"
68 | ")",
69 | dest="downloadRecipe"
70 | )
71 | recipe_args.add_argument(
72 | "--search", "-S", "--search-string",
73 | metavar="KEYWORD", default=None,
74 | help="Pass a search string to filter the local or remote recipe list",
75 | dest="searchString"
76 | )
77 | recipe_args.add_argument(
78 | "--recipe-updates", "--update-recipes", "--updates", "-U",
79 | metavar="ACTION", help="Check for recipe updates",
80 | dest="checkRecipeUpdates", default=None, choices=["check", "download"]
81 | )
82 | recipe_args.add_argument(
83 | "--kwargs", dest="kwargs", nargs="*", default={},
84 | help="Key and value pairs to pass to the recipe IE: arg1=var1,arg2=var2"
85 | )
86 |
87 | misc_args = parser.add_argument_group("misc arguments")
88 | misc_args.add_argument(
89 | "--force", action="store_true", default=False,
90 | help="Force actions that would otherwise fail", dest="forceAction"
91 | )
92 | misc_args.add_argument(
93 | "--output", "-O", "--output-type",
94 | default="json", choices=["json", "pdf", "txt", "console"],
95 | metavar="OUTPUT-TYPE", dest="outputType",
96 | help=f"Pass to control the type of output you want, default is JSON files stored in: {settings.HOME}"
97 | )
98 | misc_args.add_argument(
99 | "--hide", action="store_true", help="Hide the banner", dest="hideBanner"
100 | )
101 | misc_args.add_argument(
102 | "--version", action="store_true", help="Show version numbers and exit",
103 | dest="showVersions"
104 | )
105 |
106 | # Hidden args
107 | parser.add_argument(
108 | "--no-start-end", action="store_true", default=False, dest="noStartEnd",
109 | help=argparse.SUPPRESS
110 | )
111 | parsed = parser.parse_args()
112 |
113 | # if any of these are passed we will go ahead and hide the banner and prevent the
114 | # startup and shutdown logging
115 | if parsed.viewLocal or parsed.viewRemote or parsed.showVersions:
116 | parsed.hideBanner = True
117 | parser.noStartEnd = True
118 |
119 | # process KEY=VAL pairs from the --kwargs argument and put them into a dict
120 | # for future use
121 | kwargs_dict = {}
122 | for item in parsed.kwargs:
123 | if "=" in item:
124 | key, value = item.split("=")
125 | kwargs_dict[key] = value
126 | else:
127 | logger.warning(f"Key value pair: {item} will be skipped")
128 | parsed.kwargs = kwargs_dict
129 |
130 | return parsed
131 |
132 |
--------------------------------------------------------------------------------
/malcore_playbook/main/entry.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 12:43 PM
4 | #
5 | #
6 |
7 | import os
8 | import sys
9 | import datetime
10 | import logging
11 |
12 | import malcore_playbook.lib.cli as cli
13 | import malcore_playbook.lib.settings as settings
14 | import malcore_playbook.lib.api as api
15 | import malcore_playbook.execution.recipe_exec as recipe_exec
16 | import malcore_playbook.malscript.parse as ms_parser
17 |
18 |
19 | logger = settings.logger
20 |
21 |
22 | def main():
23 | """ tie all of it together in a single pretty function """
24 | try:
25 | if "FORCE-RELOG" in sys.argv:
26 | force = True
27 | else:
28 | force = False
29 | settings.init(force=force)
30 | parser = cli.Parser().optparse()
31 |
32 | if not parser.hideBanner:
33 | print(settings.HEADER)
34 | if parser.noStartEnd:
35 | logger.info(f"Starting up at: {datetime.datetime.now()}")
36 | settings.check_for_updates()
37 | if parser.viewRemote:
38 | _api = api.Api(only_remote=True)
39 | settings.display_recipes(_api.list_recipes(), filter_=parser.searchString)
40 | elif parser.showVersions:
41 | interp = ms_parser.MalScriptInterpreter(None)
42 | print(f"Malcore Playbook version: {settings.VERSION}")
43 | print(f"MalScript version: {interp.version}")
44 | sys.exit(1)
45 | elif parser.viewLocal:
46 | files = [f"{settings.RECIPE_HOME}{os.path.sep}{f}" for f in os.listdir(settings.RECIPE_HOME)]
47 | if len(files) == 0:
48 | logger.error("You have not downloaded any recipes, please use the --download-remote flag to start")
49 | else:
50 | dict_ = settings.create_recipe_dict_from_local(files)
51 | settings.display_recipes(dict_, filter_=parser.searchString)
52 | elif parser.checkRecipeUpdates is not None:
53 | choice = parser.checkRecipeUpdates
54 | logger.info(f"Starting to check for recipe updates, action taken: {choice}")
55 | settings.check_for_recipe_updates(force_download=True if choice == "download" else False)
56 | elif parser.downloadRecipe:
57 | _api = api.Api(only_remote=True)
58 | recipes = _api.list_recipes()
59 | selected_recipe_name = parser.downloadRecipe
60 | for selected in selected_recipe_name:
61 | if "all" in selected.lower():
62 | settings.download_all_recipes()
63 | else:
64 | for recipe in recipes:
65 | if selected in recipe['filename']:
66 | settings.download_recipe(selected, recipe, force=parser.forceAction)
67 | logger.info("Finished processing all acceptable recipes")
68 | else:
69 | if not os.path.exists(settings.HAS_INITIALIZED_DOWNLOADS):
70 | settings.logger.warning(
71 | "You have not downloaded all the currently available recipes, "
72 | "please use --download-remote all flag to download them"
73 | )
74 | if parser.useRecipe is not None:
75 | logger.debug("Checking if passed recipe is available in current recipe list")
76 | available_recipes = settings.load_recipes()
77 | passed_recipes = settings.create_recipe_list_from_passed(parser.useRecipe)
78 | for passed_recipe in passed_recipes:
79 | if passed_recipe in list(available_recipes):
80 | logger.info(f"Recipe found in available recipes, starting processing")
81 | loaded_recipe = recipe_exec.load_recipe([passed_recipe], load_one=True)
82 | if loaded_recipe is None:
83 | logger.error(f"Recipe not loaded, skipping")
84 | else:
85 | logger.debug("Starting execution of loaded recipe")
86 | try:
87 | exec_results = recipe_exec.execute_recipe(loaded_recipe, parser.filename, parser.kwargs)
88 | if exec_results is not None:
89 | logger.debug("Recipe executed successfully starting output parsing")
90 | output_file = settings.create_output_file(
91 | parser.outputType, parser.filename, passed_recipe.split(".")[0]
92 | )
93 | output_results = settings.create_output(exec_results, output_file)
94 | print(output_results)
95 | else:
96 | logger.warning("Recipe did not execute successfully, skipping")
97 | except Exception as e:
98 | logger.error(f"Unable to execute recipe successfully, got error: {str(e)}")
99 | else:
100 | logging.warning(f"Passed recipe: {passed_recipe} not found in available recipes, skipping")
101 | if parser.useChain:
102 | logger.info("User passed chain process, starting the script parsing")
103 | if parser.chainScript is None:
104 | logger.fatal("You did not pass a script to execute, please pass a chain script")
105 | else:
106 | if parser.filename is None:
107 | logger.fatal("You did not pass a filename to execute")
108 | else:
109 | chain_results = settings.execute_chain(parser.chainScript, parser.filename, **parser.kwargs)
110 | output_file = settings.create_output_file(
111 | parser.outputType, parser.filename, "recipe-chain"
112 | )
113 | output_results = settings.create_output(chain_results, output_file)
114 | print(output_results)
115 | if parser.noStartEnd:
116 | logger.debug(f"Shutting down at: {datetime.datetime.now()}")
117 | except KeyboardInterrupt:
118 | logger.fatal("User interrupted the program, shutting down")
119 | except Exception as e:
120 | import traceback
121 | traceback.print_exc()
122 | logger.fatal(f"Unhandled exception happened, MCPB is exiting: {str(e)}")
--------------------------------------------------------------------------------
/malcore_playbook/malscript/parse.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 10:29 AM
4 | #
5 | #
6 | #
7 |
8 | import re
9 |
10 | import malcore_playbook.execution.recipe_exec as recipe_exec
11 | import malcore_playbook.lib.settings as settings
12 |
13 |
14 | class MalScriptParserError(Exception):
15 |
16 | def __init__(self, msg, code, offending_line):
17 | self.msg = msg
18 | self.code = code
19 | self.offending_line = offending_line
20 | super().__init__(msg)
21 |
22 | def __str__(self):
23 | return f"[Error][{self.code}]: {self.msg}\n\t-> Code causing crash: {self.offending_line}"
24 |
25 |
26 | class ScriptSyntaxError(MalScriptParserError): pass
27 |
28 |
29 | class ScriptParserError(MalScriptParserError): pass
30 |
31 |
32 | class ScriptExecutionError(MalScriptParserError): pass
33 |
34 |
35 | class InvalidScriptPassed(FileNotFoundError): pass
36 |
37 |
38 | class MalScriptInterpreter(object):
39 |
40 | """ basic interpreter for the MalScript scripting language """
41 |
42 | def __init__(self, filename, **kwargs):
43 | self.variables = {}
44 | self.filename = filename
45 | self.kwargs = kwargs
46 | self.matcher = re.compile(r"if (.+?) in (.+?) then (.+)")
47 | self.version = "1.0"
48 |
49 | def exec_command(self, command, line_no, line):
50 | """ executes the exec() built in """
51 | settings.logger.info(f"Executing command: {command} on filename: {self.filename}")
52 | recipe = recipe_exec.load_recipe([command], load_one=True)
53 | if recipe is None:
54 | raise ScriptExecutionError(
55 | f"Failed to execute requested recipe: {command}, lino_no: {line_no}", -3, line
56 | )
57 | exec_results = recipe_exec.execute_recipe(recipe, self.filename, **self.kwargs)
58 | return exec_results
59 |
60 | def parse_value(self, value, line_no, line):
61 | """ parses and adds the variables to the storage dict """
62 | value = value.strip()
63 | type_, value = value.split("(")
64 | value = value.split(")")[0]
65 | if type_.lower() == "str":
66 | return value
67 | elif type_.lower() == "int":
68 | value = value.replace('"', "").replace("'", "")
69 | if not isinstance(value, int):
70 | return int(value)
71 | else:
72 | return value
73 | elif type_.lower() == "exec":
74 | command = value
75 | return self.exec_command(command, line_no, line)
76 | else:
77 | raise ScriptSyntaxError(
78 | f"Unsupported value type: {value}, line_no: {line_no}", -2, line
79 | )
80 |
81 | def get_nested_variables(self, var_name, condition_value, then_part, line_no, line, **kwargs):
82 | """ find nested variables data from the script """
83 | is_from_ret = kwargs.get("is_from_ret", False)
84 |
85 | if not is_from_ret:
86 | parts = var_name.split(".")
87 | var_name = parts[0]
88 | if var_name not in self.variables:
89 | raise ScriptParserError(
90 | f"Requested variable: {var_name} not found, line_no: {line_no}", -1, line
91 | )
92 | data = self.variables[var_name]
93 | keys = parts[1:]
94 | for key in keys:
95 | if key.startswith("["):
96 | try:
97 | index_number = int(key.split("[")[1].split("]")[0])
98 | data = data[index_number]
99 | except:
100 | raise ScriptSyntaxError(
101 | f"Invalid index for variable: {var_name}, line_no: {line_no}", -2, line
102 | )
103 | else:
104 | if key in data.keys():
105 | data = data.get(key)
106 | else:
107 | raise ScriptSyntaxError(
108 | f"Invalid variable: {var_name}, line_no: {line_no}", -2, line
109 | )
110 | var_value = data
111 | if isinstance(var_value, list):
112 | if any(condition_value == item for item in var_value):
113 | self.parse_value(then_part, line_no, line)
114 | else:
115 | if isinstance(condition_value, int):
116 | if condition_value == var_value:
117 | self.parse_line(then_part, line_no)
118 | else:
119 | if condition_value in str(var_value):
120 | self.parse_line(then_part, line_no)
121 | else:
122 | parts = var_name.split(".")
123 | var_name = parts[0]
124 | keys = parts[1:]
125 | data = self.variables[var_name]
126 | for key in keys:
127 | if key.startswith("["):
128 | try:
129 | index_number = int(key.split("[")[1].split("]")[0])
130 | data = data[index_number]
131 | except:
132 | raise ScriptSyntaxError(
133 | f"Invalid index for variable: {var_name}, line_no: {line_no}", -2, line
134 | )
135 | else:
136 | if key in data.keys():
137 | data = data.get(key)
138 | else:
139 | raise ScriptSyntaxError(
140 | f"Invalid variable: {var_name}, line_no: {line_no}", -2, line
141 | )
142 | return data
143 |
144 | def parse_line(self, line, line_no):
145 | """ parses the lines of the script """
146 | line_no = str(line_no)
147 | line = line.strip()
148 | if line.startswith('$') and '=' in line:
149 | settings.logger.debug(f"Found variable in script, parsing variable, line_no: {line_no}")
150 | var_name, value = line.split('=', 1)
151 | var_name = var_name.strip()
152 | value = value.strip().rstrip(';')
153 | if var_name in self.variables:
154 | settings.logger.debug(f"Variable already exists from line_no: {line_no}, overwriting")
155 | del self.variables[var_name]
156 | self.variables[var_name] = self.parse_value(value, line_no, line)
157 |
158 | elif line.startswith("#"):
159 | settings.logger.debug(f"Found a commented line on line_no: {line_no}, skipping")
160 |
161 | elif line.startswith('if '):
162 | settings.logger.debug(f"Found conditional if statement on line_no: {line_no}, parsing condition")
163 | match = self.matcher.match(line)
164 | if match:
165 | settings.logger.debug(f"Condition is acceptable, starting execution")
166 | condition_value, var_name, then_part = match.groups()
167 | condition_value = self.parse_value(condition_value, line_no, line)
168 | if isinstance(condition_value, str):
169 | condition_value = condition_value.replace("'", "").replace('"', "")
170 | var_name = var_name.strip()
171 | if var_name.startswith('$'):
172 | var_name = var_name
173 | else:
174 | var_name = '$' + var_name
175 | then_part = then_part.strip()
176 | if "." in var_name:
177 | self.get_nested_variables(var_name, condition_value, then_part, line_no, line)
178 | else:
179 | if var_name not in self.variables:
180 | raise ScriptParserError(
181 | f"Requested variable: {var_name} not found, line_no: {line_no}", -1, line
182 | )
183 | var_value = self.variables[var_name]
184 | if isinstance(var_value, list):
185 | if any(condition_value == item for item in var_value):
186 | self.parse_line(then_part, line_no)
187 | else:
188 | if condition_value in str(var_value):
189 | self.parse_line(then_part, line_no)
190 |
191 | elif line.startswith('ret'):
192 | settings.logger.debug(f"Found return statement on line_no: {line_no}, parsing return")
193 | var_name = line.split("(")[1].split(")")[0].strip()
194 | if var_name.startswith('$'):
195 | var_name = var_name
196 | else:
197 | var_name = '$' + var_name
198 | if "." in var_name:
199 | return self.get_nested_variables(
200 | var_name, None, None, line_no, line, is_from_ret=True
201 | )
202 | else:
203 | if var_name in self.variables:
204 | return self.variables[var_name]
205 | else:
206 | raise ScriptParserError(
207 | f"Variable {var_name} not found, line_no: {line_no}", -1, line
208 | )
209 |
210 | else:
211 | raise ScriptSyntaxError(
212 | f"Unknown line format: {line}, line_no: {line_no}", -2, line
213 | )
214 |
215 | def start_execution(self, script):
216 | """ starts execution of the script """
217 | settings.logger.debug(f"Starting execution of passed script")
218 | lines = script.strip().split(';')
219 | settings.logger.debug(f"Total of {len(lines)} line(s) to parse in script")
220 | result = None
221 | for i, line in enumerate(lines, start=1):
222 | if line != '':
223 | out = self.parse_line(line, i)
224 | if out is not None:
225 | result = out
226 | return result
227 |
228 |
229 | def run(script, filename, kwargs):
230 | """ pointer function to start the entire process """
231 | if kwargs is None:
232 | kwargs = {}
233 | engine = MalScriptInterpreter(filename, **kwargs)
234 | result = engine.start_execution(script)
235 | return result
236 |
--------------------------------------------------------------------------------
/malcore_playbook/lib/settings.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025.
2 | # Penetrum LLC (all rights reserved)
3 | # Copyright last updated: 5/2/25, 12:44 PM
4 | #
5 | #
6 |
7 | import os
8 | import sys
9 | import json
10 | import time
11 | import getpass
12 | import logging
13 | import datetime
14 | import hashlib
15 | from logging.handlers import RotatingFileHandler
16 | from importlib.metadata import distribution
17 |
18 | import malcore_playbook.writers.json_output as json_writer
19 | import malcore_playbook.writers.pdf_output as pdf_writer
20 | import malcore_playbook.writers.txt_output as txt_writer
21 | import malcore_playbook.writers.console_output as console_writer
22 | import malcore_playbook.malscript.parse as chain_script
23 | import malcore_playbook.lib.api as api
24 | import malcore_playbook.__version__ as version
25 |
26 | import requests
27 |
28 |
29 | class NoFilenameProvided(Exception):
30 | """ raise when there is no file provided """
31 | pass
32 |
33 |
34 | class JsonLogFormatter(logging.Formatter):
35 |
36 | """ export the log to a JSON format and log to a JSON file """
37 |
38 | def format(self, record):
39 | log_record = {
40 | "timestamp": datetime.datetime.fromtimestamp(record.created).isoformat(),
41 | "level": record.levelname,
42 | "logger": record.name,
43 | "filename": record.filename,
44 | "line": record.lineno,
45 | "message": record.getMessage(),
46 | }
47 | return json.dumps(log_record)
48 |
49 |
50 | # home directory where all config files are stored
51 | HOME = f"{os.path.expanduser('~')}{os.path.sep}.mcpb"
52 | # this is where the recipes are stored
53 | RECIPE_HOME = f"{HOME}{os.path.sep}.recipes"
54 | # RECIPE_HOME = "test-plugins"
55 | # the config file that contains the API keys and what not
56 | CONFIG_FILE = f"{HOME}{os.path.sep}config.json"
57 | # the directory that will contain all the output results
58 | OUTPUT_DIR = f"{HOME}{os.path.sep}results"
59 | # did the user accept the eula?
60 | ACCEPTED_EULA = f"{HOME}{os.path.sep}.accepted"
61 | # what plan does the user have? this isn't used yet
62 | PLAN_FILE = f"{HOME}{os.path.sep}.user_plan"
63 | # backup plan file incase the plans differ
64 | BACKUP_USER_PLAN = f"{HOME}{os.path.sep}.backup_plan"
65 | # has the user download anything yet?
66 | HAS_INITIALIZED_DOWNLOADS = f"{HOME}{os.path.sep}.initialized_downloads"
67 | # version of the program
68 | VERSION = version.VERSION
69 | # program alias nick
70 | VERSION_ALIAS = version.VERSION_ALIAS
71 | # logo header
72 | HEADER = f"""
73 | \033[91m• ▌ ▄ ·. \033[0m ▄▄· ▄▄▄·▄▄▄▄·
74 | \033[91m·██ ▐███▪\033[0m▐█ ▌▪▐█ ▄█▐█ ▀█▪
75 | \033[91m▐█ ▌▐▌▐█·\033[0m██ ▄▄ ██▀·▐█▀▀█▄
76 | \033[91m██ ██▌▐█▌\033[0m▐███▌▐█▪·•██▄▪▐█
77 | \033[91m▀▀ █▪▀▀▀·\033[0m▀▀▀ .▀ ·▀▀▀▀ v{VERSION}({VERSION_ALIAS})
78 | Malcore-Playbook
79 | """
80 | # the eula
81 | EULA = """MALCORE PLAYBOOK END USER LICENSE AGREEMENT (EULA)
82 |
83 | 1. LICENSE GRANT
84 | Malcore grants you a limited, non-exclusive, non-transferable, revocable license to use the Malcore Playbook software ("Software") solely for personal, non-commercial, and evaluation purposes during the trial period.
85 |
86 | 2. TRIAL PERIOD
87 | The Software may be used under a free trial license for evaluation purposes only. The trial period is limited to 30 days from the initial installation date, unless otherwise explicitly granted in writing by Malcore. Continuation of use beyond the trial period without obtaining a valid commercial license is strictly prohibited for any commercial, organizational, or institutional purpose.
88 |
89 | 3. COMMERCIAL USE
90 | Use of the Software by any entity other than an individual for non-commercial personal evaluation is considered Commercial Use. This includes, but is not limited to:
91 | - Use by businesses, corporations, LLCs, partnerships, government agencies, educational institutions, and non-profits;
92 | - Use on behalf of an employer;
93 | - Use in support of business operations, revenue-generating activities, cybersecurity services, malware analysis services, or internal security operations.
94 | Commercial Use requires the purchase of an active commercial license, regardless of any technical limitations or continued functionality of the Software beyond the trial period.
95 |
96 | 4. LICENSE COMPLIANCE
97 | By using the Software, you agree to comply with all license terms, including timely acquisition of a commercial license where required. Failure to obtain an appropriate license for Commercial Use constitutes a violation of this EULA and may result in legal action, penalties, or termination of access.
98 |
99 | 5. UNAUTHORIZED MODIFICATION AND CIRCUMVENTION
100 | You agree not to attempt to modify, tamper with, disable, reverse-engineer, or circumvent any technical limitations, license controls, or usage restrictions in the Software. Malcore reserves the right to audit Software usage to verify compliance.
101 |
102 | 6. OWNERSHIP
103 | Malcore retains all rights, title, and interest in and to the Software, including all intellectual property rights. No ownership or other rights are transferred under this EULA except as expressly stated.
104 |
105 | 7. DISCLAIMER OF WARRANTIES
106 | The Software is provided "AS IS" without warranties of any kind, express or implied. Malcore disclaims all warranties, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement.
107 |
108 | 8. LIMITATION OF LIABILITY
109 | In no event shall Malcore, its affiliates, or its licensors be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, arising from your use of the Software.
110 |
111 | 9. GOVERNING LAW
112 | This EULA is governed by and construed in accordance with the laws of Virginia USA.
113 |
114 | 10. AMENDMENTS
115 | Malcore reserves the right to modify or update this EULA at any time. Continued use of the Software following any changes constitutes your acceptance of those changes.
116 |
117 | USER ACCEPTANCE
118 | By installing, copying, accessing, or otherwise using the Malcore Playbook Software, you acknowledge that you have read, understood, and agree to be bound by the terms of this EULA.
119 |
120 | NOTICE: Commercial use of Malcore Playbook without an active commercial license is prohibited. Unauthorized commercial use may result in retroactive licensing fees, account suspension, and/or legal action."""
121 |
122 |
123 | def init(force=False):
124 | """ initialization function that starts the init of the program """
125 | if force:
126 | print("Forcing config refactoring")
127 | if not os.path.exists(HOME) or force:
128 | try:
129 | try:
130 | os.makedirs(HOME)
131 | os.makedirs(RECIPE_HOME)
132 | except:
133 | pass
134 | print(
135 | "You will need to accept out EULA before you begin, you will only see this once, "
136 | "you can find our terms of service here: https://malcore.io/terms-of-use"
137 | )
138 | print(f"\n{EULA}\n")
139 | is_accepted = False
140 | acceptable_answers = ('yes', 'no')
141 | while not is_accepted:
142 | answer = input("To accept type 'yes' to decline type 'no': ").strip()
143 | if answer.lower() not in list(acceptable_answers):
144 | logger.warning("Please type 'yes' or 'no'")
145 | elif answer.lower() == 'yes':
146 | open(ACCEPTED_EULA, "a+").write(f"Accepted on: {datetime.datetime.utcnow().isoformat()} UTC")
147 | is_accepted = True
148 | else:
149 | print("You have declined the EULA, this program will now exit")
150 | os.remove(HOME)
151 | return
152 | trial_file = f"{HOME}/.trial"
153 | with open(trial_file, "w") as fh:
154 | json.dump({"trial_end_date": int(time.time())}, fh)
155 | with open(PLAN_FILE, "w") as fh:
156 | fh.write("*")
157 | print("A free 30 day trial has been activated for you!")
158 | with open(CONFIG_FILE, 'w') as fh:
159 | print(
160 | "You will need to login to start using the Malcore Playbook. If you do not have an account "
161 | "please make one here: https://app.malcore.io/register"
162 | )
163 | api_ = api.Api(only_remote=True)
164 | entered = False
165 | while not entered:
166 | username = input("Enter your email: ")
167 | password = getpass.getpass("Enter your password: ")
168 | if username == "" or password == "":
169 | print("Please enter your credentials")
170 | else:
171 | data = api_.login(username, password)
172 | if data['data'] is None:
173 | print("Got error while trying to login:")
174 | for warning in data['messages']:
175 | print(f"Type: {warning['type']} Message: {warning['message']}")
176 | print("Please try again")
177 | else:
178 | key = data['data']['user']['apiKey']
179 | user_plan = data['data']['user']['subscription']['name']
180 | plan_id = data['data']['user']['subscription']['planId']
181 | file_size_limit = data['data']['user']['subscription']['fileSizeLimit']
182 | entered = True
183 | key = key.strip()
184 | json.dump({"api_key": key, "plan_id": plan_id, "file_size_limit": file_size_limit}, fh)
185 | with open(BACKUP_USER_PLAN, 'w') as fh1:
186 | fh1.write(user_plan)
187 | if force:
188 | print("Config refactored successfully, you will need to rerun the program to start")
189 | else:
190 | print("Initialization completed, you will need to rerun the program to start")
191 | exit(1)
192 | except KeyboardInterrupt:
193 | print("User quit install, removing home directory")
194 | try:
195 | os.remove(HOME)
196 | except:
197 | print(f"Failed to remove home directory do so manually: {HOME}")
198 | sys.exit(1)
199 | except Exception as e:
200 | print(f"Caught error: {str(e)}, please start the program again")
201 | try:
202 | os.remove(HOME)
203 | except:
204 | print(f"Failed to remove the HOME directory to restart install, do so manually: {HOME}")
205 | sys.exit(1)
206 | else:
207 | check_is_trial()
208 | with open(CONFIG_FILE, 'r') as fh:
209 | return json.load(fh)
210 |
211 |
212 | def setup_logger(logger_name="MalcorePlaybook"):
213 | """ logger setup """
214 | log_dir = f"{HOME}"
215 | if not os.path.exists(log_dir):
216 | init()
217 | logger = logging.getLogger(logger_name)
218 | logger.setLevel(logging.DEBUG)
219 | file_formatter = logging.Formatter('[%(asctime)s][%(module)s:%(funcName)s:%(lineno)d][%(name)s][%(levelname)s] %(message)s')
220 | output_formatter = logging.Formatter('[%(asctime)s][%(name)s][%(levelname)s] %(message)s')
221 | output_handler = logging.StreamHandler()
222 | output_handler.setLevel(logging.DEBUG)
223 | output_handler.setFormatter(output_formatter)
224 | stream_handler = RotatingFileHandler(f"{log_dir}/malcore-playbook.json", maxBytes=1_000_000, backupCount=3)
225 | stream_handler.setLevel(logging.DEBUG)
226 | stream_handler.setFormatter(JsonLogFormatter())
227 | file_handler = RotatingFileHandler(f"{log_dir}/malcore-playbook.log", maxBytes=1_000_000, backupCount=3)
228 | file_handler.setLevel(logging.DEBUG)
229 | file_handler.setFormatter(file_formatter)
230 | logger.addHandler(file_handler)
231 | logger.addHandler(output_handler)
232 | logger.addHandler(stream_handler)
233 | return logger
234 |
235 |
236 | logger = setup_logger()
237 |
238 |
239 | def load_conf():
240 | """ load the config file """
241 | with open(CONFIG_FILE, 'r') as fh:
242 | return json.load(fh)
243 |
244 |
245 | def display_recipes(recipes, filter_=None):
246 | """ display the recipes passed in a pretty format """
247 | max_display_chars = 25
248 | col_width = 35
249 | s = f"{'Recipe:'.ljust(col_width)}{'Version'.ljust(col_width)}{'Author'}"
250 | print("-" * (len(s) + 10))
251 | print(s)
252 | for recipe in recipes:
253 | if filter_ is None:
254 | recipe_name = recipe["filename"].split(".")[0]
255 | recipe_name = recipe_name[:max_display_chars]
256 | version = recipe['version'][:max_display_chars]
257 | author = recipe['author'][:max_display_chars]
258 | print(f"{recipe_name.ljust(col_width)}{version.ljust(col_width)}{author}")
259 | else:
260 | recipe_name = recipe["filename"].split(".")[0]
261 | if filter_ in recipe_name:
262 | recipe_name = recipe_name[:max_display_chars]
263 | version = recipe['version'][:max_display_chars]
264 | author = recipe['author'][:max_display_chars]
265 | print(f"{recipe_name.ljust(col_width)}{version.ljust(col_width)}{author}")
266 | print("-" * (len(s) + 10))
267 |
268 |
269 | def create_recipe_dict_from_local(local_files):
270 | """ creates a dict from local recipe files"""
271 | recipes = []
272 | for file_ in local_files:
273 | if not any(p in file_ for p in ("__pycache__", "__init__.py")):
274 | path, fname = os.path.split(file_)
275 | modulename, _ = os.path.splitext(fname)
276 | if path not in sys.path:
277 | sys.path.insert(0, path)
278 | i = __import__(modulename)
279 | author = i.__author__
280 | version = i.__version__
281 | filename = file_.split(os.path.sep)[-1]
282 | recipes.append({"filename": filename, "version": version, "author": author})
283 | return recipes
284 |
285 |
286 | def download_recipe(recipe, full_data, force=False, show_output=True):
287 | """ download the external recipes into the RECIPE directory """
288 | try:
289 | output_dir = f"{RECIPE_HOME}{os.path.sep}{recipe}.py"
290 | logger.info(f"Attempting to download recipe: {recipe} to {output_dir}")
291 | if os.path.exists(output_dir):
292 | if not force:
293 | logger.error("Recipe exists, skipping installation")
294 | return
295 | else:
296 | logger.debug("Recipe exists, forcing installation")
297 | url = f"https://recipes.malcore.io/recipes/{recipe}.py"
298 | logger.info(f"Downloading recipe from: {url}")
299 | if show_output:
300 | print(f"Recipe metadata:\n"
301 | f"\tHashsum: {full_data['hashsum']}\n"
302 | f"\tVersion: {full_data['version']}\n"
303 | f"\tAuthor: {full_data['author']}")
304 | with open(output_dir, 'wb') as fh:
305 | logger.debug("Starting download ....")
306 | with requests.get(url, stream=True) as req:
307 | req.raise_for_status()
308 | for chunk in req.iter_content(chunk_size=1024):
309 | if chunk:
310 | fh.write(chunk)
311 | logger.info(f"Recipe downloaded successfully to: {output_dir}")
312 | return True
313 | except Exception as e:
314 | logger.error(f"Hit error: {str(e)} while downloading recipe")
315 | return False
316 |
317 |
318 | def load_recipes():
319 | """ load all the local recipes """
320 | return os.listdir(RECIPE_HOME)
321 |
322 |
323 | def create_recipe_list_from_passed(passed):
324 | """ create a list of files from loaded recipes """
325 | logger.debug(f"Creating recipe list from passed, total of {len(passed)} recipe(s) to process")
326 | results = []
327 | for recipe in passed:
328 | results.append(f"{recipe}.py")
329 | return results
330 |
331 |
332 | def hash_file(filename):
333 | """ get the hash of a file """
334 | if isinstance(filename, list):
335 | filename = filename[0]
336 | with open(filename, 'rb') as fh:
337 | h = hashlib.sha256()
338 | h.update(fh.read())
339 | return h.hexdigest()
340 |
341 |
342 | def create_output_file(output_type, filename, recipe):
343 | """ creates a output file from the passed data """
344 | logger.debug(f"Creating output, passed type: {output_type}")
345 | file_hash = hash_file(filename)
346 | ext = output_type.lower()
347 | if ext != "console":
348 | if not os.path.exists(OUTPUT_DIR):
349 | os.makedirs(OUTPUT_DIR)
350 | if not os.path.exists(f"{OUTPUT_DIR}{os.path.sep}{output_type}"):
351 | os.makedirs(f"{OUTPUT_DIR}{os.path.sep}{output_type}")
352 | file_path = f"{OUTPUT_DIR}{os.path.sep}{output_type}{os.path.sep}{file_hash}-{recipe}.{ext}"
353 | logger.debug(f"Output file path generated, will use path: {file_path}")
354 | return file_path
355 | else:
356 | logger.debug("Output will be console based")
357 | return "CONSOLE"
358 |
359 |
360 | def create_output(output_results, output_file):
361 | """ creates the file that the output will be stored in """
362 | if output_file.endswith(".json"):
363 | output = json_writer.output(output_results, output_file)
364 | elif output_file.endswith("pdf"):
365 | output = pdf_writer.output(output_results, output_file)
366 | elif output_file.endswith("txt"):
367 | output = txt_writer.output(output_results, output_file)
368 | elif output_file == "CONSOLE":
369 | output = console_writer.output(output_results, output_file)
370 | else:
371 | raise NotImplementedError("That output type is not implemented yet")
372 | return output
373 |
374 |
375 | def get_user_plan():
376 | """ get the plan the user is currently using """
377 | return open(PLAN_FILE).read()
378 |
379 |
380 | def user_can_use_recipe(allowed):
381 | """ can the user execute the passed recipe? """
382 | user_plan = get_user_plan()
383 | if check_is_trial():
384 | return True
385 | if allowed is None:
386 | return True
387 | else:
388 | if allowed.lower() in user_plan.lower():
389 | return False
390 | return True
391 |
392 |
393 | def check_is_trial():
394 | """ is the user under the trial? """
395 | trial_file = f"{HOME}/.trial"
396 | if not os.path.exists(trial_file):
397 | return False
398 | else:
399 | try:
400 | data = json.load(open(trial_file))
401 | except:
402 | data = None
403 | if data is None:
404 | return False
405 | else:
406 | trial_end_timestamp = data['trial_end_date']
407 | today = datetime.datetime.utcnow()
408 | trial_end_date = datetime.datetime.utcfromtimestamp(trial_end_timestamp)
409 | trial_end_date = (trial_end_date + datetime.timedelta(days=30)).date()
410 | if today.date() > trial_end_date:
411 | logger.warning(
412 | "Your premium trial has expired, please sign up for a plan here: https://malcore.io/pricing"
413 | )
414 | try:
415 | with open(PLAN_FILE, 'w') as fh:
416 | new_plan = open(BACKUP_USER_PLAN).read()
417 | fh.write(new_plan)
418 | os.remove(trial_file)
419 | except Exception as e:
420 | logger.error(f"Failed to remove trial file, got error: {str(e)}")
421 | else:
422 | already_said = f"{HOME}/.spoken"
423 | if not os.path.exists(already_said):
424 | logger.info(
425 | f"You are currently on a premium trial membership! Your trial ends on: {trial_end_date}. "
426 | f"By using this trial you are accepting our Terms of Service: https://malcore.io/terms-of-use, "
427 | f"to upgrade your plan please see here: https://malcore.io/pricing"
428 | )
429 | open(already_said, "w").close()
430 | return True
431 |
432 |
433 | def execute_chain(script, filename, **kwargs):
434 | """ execute the MalScript """
435 | if os.path.exists(script):
436 | extension = os.path.splitext(script)[1]
437 | acceptable_extensions = (".mals", ".mal", ".ms")
438 | if any(extension == e for e in list(acceptable_extensions)):
439 | exec_script = open(script).read()
440 | else:
441 | raise chain_script.InvalidScriptPassed(
442 | f"Script extension is unverifiable (not any of: {', '.join(list(acceptable_extensions))}), "
443 | f"will not execute the script"
444 | )
445 | else:
446 | exec_script = script
447 | return chain_script.run(exec_script, filename, kwargs)
448 |
449 |
450 | def download_all_recipes(only_list=False):
451 | """ download all external recipes """
452 | percent = lambda part, whole: round((part / whole) * 100, 2)
453 | _api = api.Api(only_remote=True)
454 | data = _api.list_recipes()
455 | total_recipes = len(data)
456 | if only_list:
457 | return data
458 | total_downloaded = 0
459 | for recipe in data:
460 | is_successful = download_recipe(recipe['filename'].split(".")[0], recipe, force=True)
461 | if is_successful:
462 | total_downloaded += 1
463 | open(HAS_INITIALIZED_DOWNLOADS, "a+").close()
464 | logger.info(f"Downloaded {total_downloaded} recipes out of {total_recipes} ({percent(total_downloaded, total_recipes)}%)")
465 |
466 |
467 | def check_for_recipe_updates(force_download=False):
468 | """ check if any of the local recipes need to be updated from the external source """
469 | import malcore_playbook.execution.recipe_exec as recipe_exec
470 |
471 | updates_needed = []
472 | total_needed = 0
473 |
474 | if force_download:
475 | logger.debug("Will be downloading all recipes that need to be updated automatically")
476 |
477 | downloaded_recipes = load_recipes()
478 | remote_recipes = download_all_recipes(only_list=True)
479 | imported_recipes = recipe_exec.load_recipe(downloaded_recipes, speak=False)
480 | local_recipes = [[i.__hashsum__, i.__file__.split(os.path.sep)[-1]] for i in imported_recipes]
481 | for remote_recipe in remote_recipes:
482 | for local_recipe in local_recipes:
483 | if local_recipe[1] == remote_recipe['filename']:
484 | if not local_recipe[0] == remote_recipe['hashsum']:
485 | total_needed += 1
486 | logger.warning(
487 | f"Recipe: {local_recipe[1]} has an update available to version: {remote_recipe['version']}"
488 | )
489 | updates_needed.append(local_recipe[1])
490 | if force_download:
491 | download_recipe(remote_recipe['filename'].split(".")[0], remote_recipe, force=True)
492 | if len(updates_needed) == 0:
493 | logger.info("There are no recipes that need to be updated")
494 | else:
495 | logger.warning(f"There are a total of {total_needed} recipe(s) that require an update")
496 |
497 |
498 | def check_for_updates():
499 | """ checks if the program is up to date or not """
500 | try:
501 | current_version = VERSION
502 | url = f"https://pypi.org/pypi/malcore-playbook/json"
503 | req = requests.get(url, timeout=3)
504 | data = req.json()
505 | newest_version = data['info']['version']
506 | if current_version < newest_version:
507 | logger.warning(f"New version available, it is highly suggested that you update to version: {newest_version}")
508 | except:
509 | logger.warning("Unable to check for a newer version")
510 |
--------------------------------------------------------------------------------