diff --git a/dist/index.js b/dist/index.js index b67aff7..4a3bf20 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3953,6 +3953,235 @@ exports["default"] = _default; /***/ }), +/***/ 96: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const fs = __nccwpck_require__(147); + +async function createInterceptDotPy() { + const interceptDotPy = ` +import json +import logging +from queue import Queue +import re +from threading import Lock +from threading import Thread +import time +from OpenSSL import SSL +import os + +from mitmproxy import ctx + +import ruamel.yaml + + +FILE_WORKERS = 5 + +class Interceptor: + def __init__(self): + self.outfile = None + self.encode = None + self.url = None + self.lock = None + self.auth = None + self.queue = Queue() + self.egress_rules = None + self.mode = 'audit' + self.default_policy = 'block-all' + with open('egress_rules.yaml', 'r') as file: + yaml = ruamel.yaml.YAML(typ="safe", pure=True) + self.egress_rules = yaml.load(file) + + def done(self): + self.queue.join() + if self.outfile: + self.outfile.close() + + @classmethod + def convert_to_strings(cls, obj): + if isinstance(obj, dict): + return { + cls.convert_to_strings(key): cls.convert_to_strings(value) + for key, value in obj.items() + } + elif isinstance(obj, list) or isinstance(obj, tuple): + return [cls.convert_to_strings(element) for element in obj] + elif isinstance(obj, bytes): + return str(obj)[2:-1] + return obj + + def worker(self): + while True: + frame = self.queue.get() + self.dump(frame) + self.queue.task_done() + + def dump(self, frame): + frame["mode"] = self.mode + frame["timestamp"] = time.strftime('%X %x %Z') + frame = self.convert_to_strings(frame) + + if self.outfile: + self.lock.acquire() + self.outfile.write(json.dumps(frame) + "\n") + self.outfile.flush() + self.lock.release() + + @staticmethod + def load(loader): + loader.add_option( + "dump_destination", + str, + "jsondump.out", + "Output destination: path to a file or URL.", + ) + + def configure(self, _): + self.outfile = open(ctx.options.dump_destination, "a") + self.lock = Lock() + logging.info("Writing all data frames to %s" % ctx.options.dump_destination) + + for i in range(FILE_WORKERS): + t = Thread(target=self.worker) + t.daemon = True + t.start() + + def wildcard_to_regex(self, wildcard_domain): + regex_pattern = re.escape(wildcard_domain) # Escape special characters + regex_pattern = regex_pattern.replace(r'\*', '.*') # Replace wildcard with regex equivalent + regex_pattern = '^' + regex_pattern + '$' # Ensure the pattern matches the entire string + return re.compile(regex_pattern) + + def tls_clienthello(self, data): + default_policy = self.default_policy + + matched_rules = [] + + for rule in self.egress_rules: + domain_pattern = self.wildcard_to_regex(rule['domain']) + domain = data.client_hello.sni + if domain_pattern.match(domain) is not None: + matched_rules.append(rule) + + + data.context.matched_rules = matched_rules + + has_paths = len(matched_rules) > 0 and 'paths' in matched_rules[0] + + if has_paths: + return + + applied_rule = matched_rules[0] if len(matched_rules) > 0 else None + applied_rule_name = applied_rule.get("name", "Name not configured") if applied_rule is not None else f"Default Policy - {default_policy}" + + block = applied_rule["action"] == "block" if applied_rule is not None else default_policy == 'block-all' + + if block: + event = { + "action": "block", + "domain": domain, + "scheme": "https", + "rule_name": applied_rule_name, + } + data.context.action = "block" + if self.mode == "audit": + data.ignore_connection = True + else: + event = { + "action": "allow", + "domain": domain, + "scheme": "https", + "rule_name": applied_rule_name, + } + data.ignore_connection = True + data.context.action = "allow" + + self.queue.put(event) + + def tls_start_client(self, data): + logging.info("tls_start_client") + action = data.context.action + if action == "block" and self.mode != "audit": + data.ssl_conn = SSL.Connection(SSL.Context(SSL.SSLv23_METHOD)) + data.conn.error = f'TLS Handshake failed' + + def request(self, flow): + allow_http = False + default_policy = self.default_policy + + sni = flow.client_conn.sni + host = flow.request.pretty_host + domain = sni if sni is not None else host + scheme = flow.request.scheme + request_path = flow.request.path + + + if (not allow_http) and scheme == "http": + event = { + "action": "block", + "domain": domain, + "scheme": "http", + "rule_name": "allow_http is False" + } + self.queue.put(event) + if self.mode != "audit": + flow.kill() + return + + block = default_policy == 'block-all' + breakFlag = False + applied_rule = None + + for rule in self.egress_rules: + domain_pattern = self.wildcard_to_regex(rule['domain']) + if domain_pattern.match(domain) is not None: + paths = rule.get('paths', []) + if len(paths) == 0: + block = rule['action'] == 'block' + applied_rule = rule + break + for path in paths: + path_regex = self.wildcard_to_regex(path) + if path_regex.match(request_path) is not None: + block = rule['action'] == 'block' + applied_rule = rule + breakFlag = True + break + if breakFlag: + break + + applied_rule_name = applied_rule.get("name", "Name not configured") if applied_rule is not None else f"Default Policy - {default_policy}" + + if block: + event = { + "action": "block", + "domain": domain, + "scheme": scheme, + "rule_name": applied_rule_name + } + if self.mode != "audit": + flow.kill() + else: + event = { + "action": "allow", + "domain": domain, + "scheme": scheme, + "rule_name": applied_rule_name + } + + self.queue.put(event) + +addons = [Interceptor()] # pylint: disable=invalid-name +` + fs.writeFileSync('intercept.py', interceptDotPy); +} + +createInterceptDotPy() + +module.exports = { createInterceptDotPy } + +/***/ }), + /***/ 713: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { @@ -3960,6 +4189,7 @@ const core = __nccwpck_require__(186) const io = __nccwpck_require__(436) const { exec } = __nccwpck_require__(514) const { wait } = __nccwpck_require__(312) +const { createInterceptDotPy } = __nccwpck_require__(96) /** * The main function for the action. @@ -3989,24 +4219,31 @@ async function run() { core.startGroup("run-bolt") core.info("Starting bolt...") - const createBoltOutputFileCommand = ```sudo -u mitmproxyuser -H bash -c \ + const createBoltOutputFileCommand = `sudo -u mitmproxyuser -H bash -c \ 'touch /home/mitmproxyuser/output.log' - ``` + ` exec(createBoltOutputFileCommand) - const createBolConfigCommand = ```sudo -u mitmproxyuser -H bash -c \ + const createBolConfigCommand = `sudo -u mitmproxyuser -H bash -c \ 'mkdir -p /home/mitmproxyuser/.mitmproxy && \ echo "dump_destination: \"/home/mitmproxyuser/output.log\"" > ~/.mitmproxy/config.yaml' - ``` - exec(createBolConfigCommand) + ` + exec(createBolConfigCommand) + + fs.writeFileSync('egress_rules.yaml', core.getInput('egress_rules')); + + createInterceptDotPy() + + exec('sudo cp intercept.py /home/mitmproxyuser/intercept.py && sudo chown mitmproxyuser:mitmproxyuser /home/mitmproxyuser/intercept.py') + exec('sudo cp egress_rules.yaml /home/mitmproxyuser/egress_rules && sudo chown mitmproxyuser:mitmproxyuser /home/mitmproxyuser/egress_rules') - const runBoltCommand =```sudo -u mitmproxyuser -H bash -c \ + const runBoltCommand =`sudo -u mitmproxyuser -H bash -c \ 'BOLT_MODE=${{mode}} \ BOLT_ALLOW_HTTP=${{allow_http}} \ $BOLT_DEFAULT_POLICY=${{default_policy}} \ $HOME/.local/bin/mitmdump --mode transparent --showhost --set block_global=false \ -s .github/actions/bolt/intercept.py &' - ``` + ` exec(runBoltCommand) core.info("Waiting for bolt to start...") @@ -4017,7 +4254,18 @@ async function run() { core.endGroup("run-bolt") - + + core.startGroup("setup-iptables-redirection") + exec("sudo sysctl -w net.ipv4.ip_forward=1") + exec("sudo sysctl -w net.ipv6.conf.all.forwarding=1") + exec("sudo sysctl -w net.ipv4.conf.all.send_redirects=0") + exec("sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitmproxyuser --dport 80 -j REDIRECT --to-port 8080") + exec("sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitmproxyuser --dport 443 -j REDIRECT --to-port 8080") + exec("sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitmproxyuser --dport 80 -j REDIRECT --to-port 8080") + exec("sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitmproxyuser --dport 443 -j REDIRECT --to-port 8080") + core.endGroup("setup-iptables-redirection") + + } catch (error) { // Fail the workflow run if an error occurs @@ -4037,94 +4285,96 @@ module.exports = { const core = __nccwpck_require__(186) const { exec } = __nccwpck_require__(514) -const fs = __nccwpck_require__(147); - - +const fs = __nccwpck_require__(147) function generateTestResults() { - // Specify the path to your file - exec("sudo cp /home/mitmproxyuser/output.log output.log") - exec("sudo chown -R $USER:$USER output.log") - const filePath = 'output.log'; - - try { - // Read the entire file synchronously and split it into an array of lines - const fileContent = fs.readFileSync(filePath, 'utf-8'); - const lines = fileContent.split('\n'); - - // Initialize an empty array to store JSON objects - const jsonArray = []; - - // Iterate through each line and parse it as JSON - lines.forEach((line) => { - try { - const jsonObject = JSON.parse(line); - jsonArray.push(jsonObject); - } catch (error) { - console.error(`Error parsing JSON on line: ${line}`); - } - }); + // Specify the path to your file + exec('sudo cp /home/mitmproxyuser/output.log output.log') + exec('sudo chown -R $USER:$USER output.log') + const filePath = 'output.log' - return jsonArray; - } catch (error) { - console.error(`Error reading file: ${error.message}`); - } + try { + // Read the entire file synchronously and split it into an array of lines + const fileContent = fs.readFileSync(filePath, 'utf-8') + const lines = fileContent.split('\n') + + // Initialize an empty array to store JSON objects + const jsonArray = [] + + // Iterate through each line and parse it as JSON + lines.forEach(line => { + try { + const jsonObject = JSON.parse(line) + jsonArray.push(jsonObject) + } catch (error) { + console.error(`Error parsing JSON on line: ${line}`) + } + }) + return jsonArray + } catch (error) { + console.error(`Error reading file: ${error.message}`) + } } function actionIcon(action) { - switch (action) { - case 'block': - return '❌'; - case 'allow': - return '✅'; - default: - return '❔'; - } + switch (action) { + case 'block': + return '❌' + case 'allow': + return '✅' + default: + return '❔' + } } function getUniqueBy(arr, keys) { - const uniqueObj = arr.reduce((unique, o) => { - const key = keys.map((k) => o[k]).join('|'); - unique[key] = o; - return unique; - }, {}); - return Object.values(uniqueObj); + const uniqueObj = arr.reduce((unique, o) => { + const key = keys.map(k => o[k]).join('|') + unique[key] = o + return unique + }, {}) + return Object.values(uniqueObj) } async function summary() { - const results = generateTestResults(); - const uniqueResults = getUniqueBy(results, ['domain', 'scheme']).map((result) => [ - result.domain, - result.scheme, - result.rule_name, - actionIcon(result.action), - ]);; - - const table = [ - [ - {data: 'Domain', header: true}, - {data: 'Scheme', header: true}, - {data: 'Rule', header: true}, - {data: 'Action', header: true}, - ], - ...uniqueResults, + const results = generateTestResults() + const uniqueResults = getUniqueBy(results, ['domain', 'scheme']).map( + result => [ + result.domain, + result.scheme, + result.rule_name, + actionIcon(result.action) ] - - console.log("Koalalab-inc-egress-traffic-report>>>>>>>>>>"); - console.log(JSON.stringify(results)); - console.log("<<<<<<<<<>>>>>>>>>') + console.log(JSON.stringify(results)) + console.log('<<<<<<<<<