diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..85e2091 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,172 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [4.10.0] + +### Changed + +- Added post-install validations for the Wazuh manager and Filebeat. ([#3059](https://github.com/wazuh/wazuh-packages/pull/3059)) + +### Fixed + +- Fixed Wazuh API validation ([#29](https://github.com/wazuh/wazuh-installation-assistant/pull/29)) + +## [4.9.1] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.9.1 + +## [4.9.0] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.9.0 + +## [4.8.1] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.8.1 + +## [4.8.0] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.8.0 + +## [4.7.5] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.7.5 + +## [4.7.4] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.7.4 + +## [4.7.3] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.7.3 + +## [4.7.2] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.7.2 + +## [4.7.1] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.7.1 + +## [v4.7.0] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.7.0 + +## [v4.6.0] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.6.0 + +## [v4.5.4] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.5.4 + +## [v4.5.3] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.5.3 + +## [v4.5.2] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.5.2 + +## [v4.5.1] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.5.1 + +## [v4.5.0] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.5.0 + +## [v4.4.5] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.4.5 + +## [v4.4.4] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.4.4 + +## [v4.4.3] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.4.3 + +## [v4.4.2] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.4.2 + +## [v4.3.11] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.11 + +## [v4.4.1] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.4.1 + +## [v4.4.0] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.4.0 + +## [v4.3.10] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.10 + +## [v4.3.9] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.9 + +## [v4.3.8] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.8 + +## [v4.3.7] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.7 + +## [v4.3.6] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.6 + +## [v4.3.5] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.5 + +## [v4.3.4] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.4 + +## [v4.3.3] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.3 + +## [v4.3.2] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.2 + +## [v4.2.7] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.2.7 + +## [v4.3.1] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.1 + +## [v4.3.0] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.3.0 + +## [v4.2.7] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.2.7 + +## [v4.2.6] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.2.7 + +## [v4.2.5] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.2.5 + +## [v4.2.4] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.2.4 + +## [v4.2.3] + +- https://github.com/wazuh/wazuh-packages/releases/tag/v4.2.3 diff --git a/Development-guide.md b/Development-guide.md deleted file mode 100644 index aeab2a0..0000000 --- a/Development-guide.md +++ /dev/null @@ -1,46 +0,0 @@ -Wazuh unattended installer - Development guide -======================================== - -[![Slack](https://img.shields.io/badge/slack-join-blue.svg)](https://wazuh.com/community/join-us-on-slack/) -[![Email](https://img.shields.io/badge/email-join-blue.svg)](https://groups.google.com/forum/#!forum/wazuh) -[![Documentation](https://img.shields.io/badge/docs-view-green.svg)](https://documentation.wazuh.com) -[![Documentation](https://img.shields.io/badge/web-view-green.svg)](https://wazuh.com) - -# Development guide - -In order to have homogenous developments, this document shows some rules to be taken into account when adding or modifying capabilities. These rules must be taken into account as much as possible. - -- Write a function with a single objective. -- Every function must have limited arguments, the less the better. -- The functions should be open for extension but closed for modifications. -- Use libraries (I.e `install_functions`) and load only the necessaries. -- Main functions will not depend on the functions themselves implementation. -- Use descriptive variable names and function names. Avoid mind-mapping, a clean code is itself commented. Use comments only in necessary cases. -- Use Read-only to declare static variables. -- Use `$(command)` instead of classic `` `command` ``. -- Use `${var}` instead of `$(var)`. -- Variables should always be quoted: `"${var}"`. -- Use logger function instead of `echo`. -- Prevent commands from failure by catching the result of a command with `$?`. -- Take control of every possible long command setting up timeouts. -- Every needed resource must be obtained (on-line and off-line). This resource must be checked if exist in the desired path (libraries). -- Use `command -v` for commands exist checking. -- Parametrize all packages versions. -- Use `| grep -q` instead of `| grep` -- Use standard `$((..))` instead of old `$[]` - -*Additional check*: Run unit [tests](/tests/unattended/unit/README) before preparing a pull request. - -## License and copyright - -Copyright (C) 2015, Wazuh Inc. - -This program is free software; you can redistribute it -and/or modify it under the terms of the GNU General Public -License (version 2) as published by the FSF - Free Software -Foundation. - -## Useful links and acknowledgment - -- [Bash meets solid](https://codewizardly.com/bash-meets-solid/) -- [Shellcheck](https://github.com/koalaman/shellcheck#gallery-of-bad-code) diff --git a/README.md b/README.md index 280bb7a..dad0bdc 100644 --- a/README.md +++ b/README.md @@ -1 +1,176 @@ -# wazuh-installation-assistant \ No newline at end of file +# Wazuh installation assistant + +[![Slack](https://img.shields.io/badge/slack-join-blue.svg)](https://wazuh.com/community/join-us-on-slack/) +[![Email](https://img.shields.io/badge/email-join-blue.svg)](https://groups.google.com/forum/#!forum/wazuh) +[![Documentation](https://img.shields.io/badge/docs-view-green.svg)](https://documentation.wazuh.com) +[![Documentation](https://img.shields.io/badge/web-view-green.svg)](https://wazuh.com) +[![Twitter](https://img.shields.io/twitter/follow/wazuh?style=social)](https://twitter.com/wazuh) +[![YouTube](https://img.shields.io/youtube/views/peTSzcAueEc?style=social)](https://www.youtube.com/watch?v=peTSzcAueEc) + +## Table of Contents +1. [Overview](#overview) +2. [Tools](#tools) +3. [User Guide](#user-guide) +4. [Use Cases](#use-cases) +5. [Options Table](#options-table) +6. [Contribute](#contribute) +7. [Development Guide](#development-guide) +7. [More Information](#more-information) +9. [Authors](#authors) + +## Overview + +The Wazuh installation Assistant is a tool designed to simplify the deployment of Wazuh. It guides users through the process of installing Wazuh components. Key features include: + +- **Guided Installation**: Step-by-step instructions for easy setup. +- **Component Selection**: Install only the Wazuh components you need. +- **System Requirements Check**: Automatically checks if your system meets the necessary requirements. +- **Automated Configuration**: Reduces errors by automating most of the setup. +- **Multi-Platform Support**: Compatible with various Linux distributions like Ubuntu, CentOS, and Debian. + +## Tools + +The Wazuh installation assistant uses the following tools to enhance security during the installation process: + +- **Wazuh password tool**: Securely generate and manage passwords. [Learn more](https://documentation.wazuh.com/current/user-manual/user-administration/password-management.html). +- **Wazuh cert tool**: Manage SSL/TLS certificates for secure communications. [Learn more](https://documentation.wazuh.com/current/user-manual/wazuh-dashboard/certificates.html). + + + +## User Guide + +### Downloads +- [Download the Wazuh installation assistant.](https://packages.wazuh.com/4.10/wazuh-install.sh) +- [Download the Wazuh password tool.](https://packages.wazuh.com/4.10/wazuh-passwords-tool.sh) +- [Download the Wazuh cert tool.](https://packages.wazuh.com/4.10/wazuh-certs-tool.sh) + +### Build the scripts +As an alternative to downloading, use the `builder.sh` script to build the Wazuh installation assistant and tools: + + +1. Build the Wazuh installation assistant - `wazuh-install.sh`: + ```bash + bash builder.sh -i + ``` + +2. Build the Wazuh password tool - `wazuh-passwords-tool.sh`: + ```bash + bash builder.sh -p + ``` + +3. Build the Wazuh cert tool - `wazuh-certs-tool.sh`: + ```bash + bash builder.sh -c + ``` + +## Use Cases + +Start by downloading the [configuration file](https://packages.wazuh.com/4.10/config.yml) and replace the node names and IP values with the corresponding ones. + +> [!NOTE] +> It is not necessary to download the Wazuh password tool and the Wazuh cert tool to use the Wazuh installation assistant. The Wazuh installation assistant has embedded the previous tools. + +### Common commands + +1. Generate the passwords and certificates. Needs the [configuration file](https://packages.wazuh.com/4.10/config.yml). + ```bash + bash wazuh-install.sh -g + ``` +2. Install all central components on the local machine: + ```bash + bash wazuh-install.sh -a + ``` + +3. Uninstall all central components: + ```bash + bash wazuh-install.sh -u + ``` + +4. Install the Wazuh indexer specifying the same name as specified in the configuration file: + ```bash + bash wazuh-install.sh --wazuh-indexer + ``` + +5. Initialize the Wazuh indexer cluster: + ```bash + bash wazuh-install.sh --start-cluster + ``` + +6. Install the Wazuh server specifying the same name as specified in the configuration file: + ```bash + bash wazuh-install.sh --wazuh-server + ``` + +7. Install the Wazuh dashboard specifying the same name as specified in the configuration file: + ```bash + bash wazuh-install.sh --wazuh-dashboard + ``` + +8. Display all options and help: + ```bash + bash wazuh-install.sh -h + ``` + +## Options Table + +All the options for the Wazuh installation assistant are listed in the following table: +| Option | Description | +|---------------------------------------|----------------------------------------| +| `-a`, `--all-in-one` | Install and configure Wazuh server, Wazuh indexer, Wazuh dashboard. | +| `-c`, `--config-file ` | Path to the configuration file used to generate `wazuh-install-files.tar` file containing the files needed for installation. By default, the Wazuh installation assistant will search for a file named `config.yml` in the same path as the script. | +| `-dw`, `--download-wazuh ` | Download all the packages necessary for offline installation. Specify the type of packages to download for offline installation (`rpm`, `deb`). | +| `-fd`, `--force-install-dashboard` | Force Wazuh dashboard installation to continue even when it is not capable of connecting to the Wazuh indexer. | +| `-g`, `--generate-config-files` | Generate `wazuh-install-files.tar` file containing the files needed for installation from `config.yml`. In distributed deployments, you will need to copy this file to all hosts. | +| `-h`, `--help` | Display this help and exit. | +| `-i`, `--ignore-check` | Ignore the check for minimum hardware requirements. | +| `-o`, `--overwrite` | Overwrite previously installed components. This will erase all the existing configuration and data. | +| `-of`, `--offline-installation` | Perform an offline installation. This option must be used with `-a`, `-ws`, `-s`, `-wi`, or `-wd`. | +| `-p`, `--port` | Specify the Wazuh web user interface port. Default is the `443` TCP port. Recommended ports are: `8443`, `8444`, `8080`, `8888`, `9000`. | +| `-s`, `--start-cluster` | Initialize Wazuh indexer cluster security settings. | +| `-t`, `--tar ` | Path to tar file containing certificate files. By default, the Wazuh installation assistant will search for a file named `wazuh-install-files.tar` in the same path as the script. | +| `-u`, `--uninstall` | Uninstall all Wazuh components. This will erase all the existing configuration and data. | +| `-v`, `--verbose` | Show the complete installation output. | +| `-V`, `--version` | Show the version of the script and Wazuh packages. | +| `-wd`, `--wazuh-dashboard ` | Install and configure Wazuh dashboard, used for distributed deployments. | +| `-wi`, `--wazuh-indexer ` | Install and configure Wazuh indexer, used for distributed deployments. | +| `-ws`, `--wazuh-server ` | Install and configure Wazuh manager and Filebeat, used for distributed deployments. | + + +## Contribute + +If you want to contribute to our repository, please fork our GitHub repository and submit a pull request. Alternatively, you can share ideas through [our users' mailing list](https://groups.google.com/d/forum/wazuh). + +## Development Guide + +To ensure consistency in development, please follow these guidelines: + +- Write functions with a single objective and limited arguments. +- Use libraries selectively (e.g., `install_functions`). +- Main functions should not depend on specific implementations. +- Use descriptive names for variables and functions. +- Use `${var}` instead of `$(var)` and `$(command)` instead of backticks. +- Always quote variables: `"${var}"`. +- Use the `common_logger` function instead of `echo`. +- Check command results with `$?` or `PIPESTATUS`. +- Use timeouts for long commands. +- Ensure all necessary resources are available both online and offline. +- Check command existence with `command -v`. +- Parametrize all package versions. +- Use `| grep -q` instead of `| grep`. +- Use standard `$((..))` instead of old `$[]`. + +> [!TIP] +> *Additional check*: Run unit [tests](/tests/unit/README) before preparing a pull request. + +Some useful links and acknowledgment: +- [Bash meets solid](https://codewizardly.com/bash-meets-solid/) +- [Shellcheck](https://github.com/koalaman/shellcheck#gallery-of-bad-code) + +## More Information + +For more detailed instructions and advanced use cases, please refer to the [Wazuh Quickstart Guide](https://documentation.wazuh.com/current/quickstart.html). + + +## Authors + +Wazuh Copyright (C) 2015-2023 Wazuh Inc. (License GPLv2) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..100f730 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# Wazuh Open Source Project Security Policy + +Version: 2023-06-12 + +## Introduction +This document outlines the Security Policy for Wazuh's open source projects. It emphasizes our commitment to maintain a secure environment for our users and contributors, and reflects our belief in the power of collaboration to identify and resolve security vulnerabilities. + +## Scope +This policy applies to all open source projects developed, maintained, or hosted by Wazuh. + +## Reporting Security Vulnerabilities +If you believe you've discovered a potential security vulnerability in one of our open source projects, we strongly encourage you to report it to us responsibly. + +Please submit your findings as [security advisories](https://github.com/wazuh/wazuh-installation-assistant/security/advisories) under the "Security" tab in the relevant GitHub repository. Alternatively, you may send the details of your findings to security@wazuh.com. + +## Vulnerability Disclosure Policy +Upon receiving a report of a potential vulnerability, our team will initiate an investigation. If the reported issue is confirmed as a vulnerability, we will take the following steps: + +1. Acknowledgment: We will acknowledge the receipt of your vulnerability report and begin our investigation. + +2. Validation: We will validate the issue and work on reproducing it in our environment. + +3. Remediation: We will work on a fix and thoroughly test it + +4. Release & Disclosure: After 90 days from the discovery of the vulnerability, or as soon as a fix is ready and thoroughly tested (whichever comes first), we will release a security update for the affected project. We will also publicly disclose the vulnerability by publishing a CVE (Common Vulnerabilities and Exposures) and acknowledging the discovering party. + +5. Exceptions: In order to preserve the security of the Wazuh community at large, we might extend the disclosure period to allow users to patch their deployments. + +This 90-day period allows for end-users to update their systems and minimizes the risk of widespread exploitation of the vulnerability. + +## Automatic Scanning +We leverage GitHub Actions to perform automated scans of our supply chain. These scans assist us in identifying vulnerabilities and outdated dependencies in a proactive and timely manner. + +## Credit +We believe in giving credit where credit is due. If you report a security vulnerability to us, and we determine that it is a valid vulnerability, we will publicly credit you for the discovery when we disclose the vulnerability. If you wish to remain anonymous, please indicate so in your initial report. + +We do appreciate and encourage feedback from our community, but currently we do not have a bounty program. We might start bounty programs in the future. + +## Compliance with this Policy +We consider the discovery and reporting of security vulnerabilities an important public service. We encourage responsible reporting of any vulnerabilities that may be found in our site or applications. + +Furthermore, we will not take legal action against or suspend or terminate access to the site or services of those who discover and report security vulnerabilities in accordance with this policy because of the fact. + +We ask that all users and contributors respect this policy and the security of our community's users by disclosing vulnerabilities to us in accordance with this policy. + +## Changes to this Security Policy +This policy may be revised from time to time. Each version of the policy will be identified at the top of the page by its effective date. + +If you have any questions about this Security Policy, please contact us at security@wazuh.com diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..2da4316 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +4.10.0 diff --git a/builder.sh b/builder.sh index 58087ae..f007604 100755 --- a/builder.sh +++ b/builder.sh @@ -22,7 +22,7 @@ function getHelp() { echo -e "" echo -e "NAME" - echo -e " $(basename "$0") - Build unattended installation files." + echo -e " $(basename "$0") - Builds the Wazuh installation assistant and tools." echo -e "" echo -e "SYNOPSIS" echo -e " $(basename "$0") [-v] -i | -c | -p" diff --git a/config/dashboard/dashboard_unattended.yml b/config/dashboard/dashboard_assistant.yml similarity index 100% rename from config/dashboard/dashboard_unattended.yml rename to config/dashboard/dashboard_assistant.yml diff --git a/config/dashboard/dashboard_unattended_distributed.yml b/config/dashboard/dashboard_assistant_distributed.yml similarity index 100% rename from config/dashboard/dashboard_unattended_distributed.yml rename to config/dashboard/dashboard_assistant_distributed.yml diff --git a/config/filebeat/filebeat_unattended.yml b/config/filebeat/filebeat_assistant.yml similarity index 95% rename from config/filebeat/filebeat_unattended.yml rename to config/filebeat/filebeat_assistant.yml index 81a8ddf..17927ab 100644 --- a/config/filebeat/filebeat_unattended.yml +++ b/config/filebeat/filebeat_assistant.yml @@ -1,7 +1,7 @@ # Wazuh - Filebeat configuration file output.elasticsearch.hosts: - 127.0.0.1:9200 -# - :9200 +# - :9200 # - :9200 output.elasticsearch: diff --git a/config/indexer/indexer_unattended_distributed.yml b/config/indexer/indexer_assistant_distributed.yml similarity index 100% rename from config/indexer/indexer_unattended_distributed.yml rename to config/indexer/indexer_assistant_distributed.yml diff --git a/install_functions/dashboard.sh b/install_functions/dashboard.sh index 2b43fb5..97455fb 100644 --- a/install_functions/dashboard.sh +++ b/install_functions/dashboard.sh @@ -21,10 +21,10 @@ function dashboard_configure() { common_logger -d "Configuring Wazuh dashboard." if [ -n "${AIO}" ]; then - eval "installCommon_getConfig dashboard/dashboard_unattended.yml /etc/wazuh-dashboard/opensearch_dashboards.yml ${debug}" + eval "installCommon_getConfig dashboard/dashboard_assistant.yml /etc/wazuh-dashboard/opensearch_dashboards.yml ${debug}" dashboard_copyCertificates "${debug}" else - eval "installCommon_getConfig dashboard/dashboard_unattended_distributed.yml /etc/wazuh-dashboard/opensearch_dashboards.yml ${debug}" + eval "installCommon_getConfig dashboard/dashboard_assistant_distributed.yml /etc/wazuh-dashboard/opensearch_dashboards.yml ${debug}" dashboard_copyCertificates "${debug}" if [ "${#dashboard_node_names[@]}" -eq 1 ]; then pos=0 diff --git a/install_functions/filebeat.sh b/install_functions/filebeat.sh index 9fef47d..30f039f 100644 --- a/install_functions/filebeat.sh +++ b/install_functions/filebeat.sh @@ -46,7 +46,7 @@ function filebeat_configure(){ eval "chmod go+r /etc/filebeat/wazuh-template.json ${debug}" if [ -n "${AIO}" ]; then - eval "installCommon_getConfig filebeat/filebeat_unattended.yml /etc/filebeat/filebeat.yml ${debug}" + eval "installCommon_getConfig filebeat/filebeat_assistant.yml /etc/filebeat/filebeat.yml ${debug}" else eval "installCommon_getConfig filebeat/filebeat_distributed.yml /etc/filebeat/filebeat.yml ${debug}" if [ ${#indexer_node_names[@]} -eq 1 ]; then diff --git a/install_functions/indexer.sh b/install_functions/indexer.sh index 5345fd2..d224e5e 100644 --- a/install_functions/indexer.sh +++ b/install_functions/indexer.sh @@ -24,7 +24,7 @@ function indexer_configure() { if [ -n "${AIO}" ]; then eval "installCommon_getConfig indexer/indexer_all_in_one.yml /etc/wazuh-indexer/opensearch.yml ${debug}" else - eval "installCommon_getConfig indexer/indexer_unattended_distributed.yml /etc/wazuh-indexer/opensearch.yml ${debug}" + eval "installCommon_getConfig indexer/indexer_assistant_distributed.yml /etc/wazuh-indexer/opensearch.yml ${debug}" if [ "${#indexer_node_names[@]}" -eq 1 ]; then pos=0 { @@ -213,6 +213,4 @@ function indexer_startCluster() { else common_logger -d "Inserted wazuh-alerts template into the Wazuh indexer cluster." fi - - } diff --git a/install_functions/manager.sh b/install_functions/manager.sh index a33e564..862f76b 100644 --- a/install_functions/manager.sh +++ b/install_functions/manager.sh @@ -47,7 +47,7 @@ function manager_checkService() { eval "TOKEN=$(curl -k -s -X POST -u "wazuh-wui:wazuh-wui" https://127.0.0.1:55000/security/user/authenticate/run_as?raw=true -d '{"user_name":"wzread"}' -H "content-type:application/json")" wm_error=$(curl -k -s -X GET "https://127.0.0.1:55000/agents/outdated?pretty=true" -H "Authorization: Bearer ${TOKEN}") - if [[ ${wm_error,,} = '"error": 0' ]]; then + if [[ ${wm_error,,} =~ '"error": 0' ]]; then common_logger "Wazuh API connection successful" else common_logger -e "Wazuh API connection Error. $wm_error" diff --git a/tests/install/pytest.ini b/tests/install/pytest.ini new file mode 100644 index 0000000..3827ff1 --- /dev/null +++ b/tests/install/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +filterwarnings = + ignore:Unverified HTTPS request is being made.* +markers = + wazuh: tests to be executed on Wazuh hosts (does not include wazuh-clusterd test). + wazuh_cluster: test for wazuh-clusterd it is meant to be executed on the master node if a wazuh cluster is configured. + wazuh_worker: test for wazuh cluster worker nodes. It is meant to be executed on the worker nodes. + indexer: tests to be executed on Wazuh Indexer hosts. + indexer_cluster: tests to be executed on Wazuh Indexer hosts on distributed installations. + dashboard: tests to be executed on Wazuh dashboard hosts. \ No newline at end of file diff --git a/tests/install/test_installation_assistant.py b/tests/install/test_installation_assistant.py new file mode 100644 index 0000000..0cdd7df --- /dev/null +++ b/tests/install/test_installation_assistant.py @@ -0,0 +1,280 @@ +from datetime import datetime +import pytest +import json +import sys +import tarfile +from subprocess import Popen, PIPE, check_output +import yaml +import requests +import socket +from base64 import b64encode +import warnings +import subprocess +from subprocess import check_call + +warnings.filterwarnings('ignore', message='Unverified HTTPS request') + +# ----------------------------- Aux functions ----------------------------- + +def read_services(): + services = None + p = Popen(['/var/ossec/bin/wazuh-control', 'status'], stdin=PIPE, stdout=PIPE, stderr=PIPE) + if sys.version_info[0] < 3: + services = p.stdout.read() + else: + services = p.stdout + p.kill() + +def get_password(username): + pass_dict={'username': 'tmp_user', 'password': 'tmp_pass'} + tmp_yaml="" + + with tarfile.open("../../wazuh-install-files.tar") as configurations: + configurations.extract("wazuh-install-files/wazuh-passwords.txt") + + with open("wazuh-install-files/wazuh-passwords.txt", 'r') as pass_file: + while pass_dict["username"] != username: + for i in range(4): + tmp_yaml+=pass_file.readline() + tmp_dict=yaml.safe_load(tmp_yaml) + if 'indexer_username' in tmp_dict: + pass_dict["username"]=tmp_dict["indexer_username"] + pass_dict["password"]=tmp_dict["indexer_password"] + if 'api_username' in tmp_dict: + pass_dict["username"]=tmp_dict["api_username"] + pass_dict["password"]=tmp_dict["api_password"] + return pass_dict["password"] + +def get_wazuh_version(): + wazuh_version = None + wazuh_version = subprocess.getoutput('/var/ossec/bin/wazuh-control info | grep VERSION | cut -d "=" -f2 | sed s/\\"//g') + return wazuh_version + +def get_indexer_ip(): + + with open("/etc/wazuh-indexer/opensearch.yml", 'r') as stream: + dictionary = yaml.safe_load(stream) + return (dictionary.get('network.host')) + +def get_dashboard_ip(): + + with open("/etc/wazuh-dashboard/opensearch_dashboards.yml", 'r') as stream: + dictionary = yaml.safe_load(stream) + return (dictionary.get('server.host')) + +def get_api_ip(): + + with open("/var/ossec/api/configuration/api.yaml", 'r') as stream: + dictionary = yaml.safe_load(stream) + try: + ip = dictionary.get('host') + except: + ip = '127.0.0.1' + return ip + +def api_call_indexer(host,query,address,api_protocol,api_user,api_pass,api_port): + + if (query == ""): # Calling ES API without query + if (api_user != "" and api_pass != ""): # If credentials provided + resp = requests.get(api_protocol + '://' + address + ':' + api_port, + auth=(api_user, + api_pass), + verify=False) + else: + resp = requests.get(api_protocol + '://' + address + ':' + api_port, verify=False) + + else: # Executing query search + if (api_pass != "" and api_pass != ""): + resp = requests.post(api_protocol + '://' + address + ':' + api_port + "/wazuh-alerts-4.x-*/_search", + json=query, + auth=(api_user, + api_pass), + verify=False) + else: + resp = requests.get(api_protocol + "://" + address + ":" + api_port) + response = resp.json() + return response + +def get_indexer_cluster_status(): + ip = get_indexer_ip() + resp = requests.get('https://'+ip+':9200/_cluster/health', + auth=("admin", + get_password("admin")), + verify=False) + return (resp.json()['status']) + +def get_dashboard_status(): + ip = get_dashboard_ip() + resp = requests.get('https://'+ip, + auth=("kibanaserver", + get_password("kibanaserver")), + verify=False) + return (resp.status_code) + +def get_wazuh_api_status(): + + protocol = 'https' + host = get_api_ip() + port = 55000 + user = 'wazuh' + password = get_password('wazuh') + login_endpoint = 'security/user/authenticate' + + login_url = f"{protocol}://{host}:{port}/{login_endpoint}" + basic_auth = f"{user}:{password}".encode() + login_headers = {'Content-Type': 'application/json', + 'Authorization': f'Basic {b64encode(basic_auth).decode()}'} + response = requests.post(login_url, headers=login_headers, verify=False) + token = json.loads(response.content.decode())['data']['token'] + requests_headers = {'Content-Type': 'application/json', + 'Authorization': f'Bearer {token}'} + response = requests.get(f"{protocol}://{host}:{port}/?pretty=true", headers=requests_headers, verify=False) + return response.json()['data']['title'] + +# ----------------------------- Tests ----------------------------- + +@pytest.mark.wazuh +def test_check_wazuh_manager_authd(): + assert check_call("ps -xa | grep wazuh-authd | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_db(): + assert check_call("ps -xa | grep wazuh-db | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_execd(): + assert check_call("ps -xa | grep wazuh-execd | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_analysisd(): + assert check_call("ps -xa | grep wazuh-analysisd | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_syscheckd(): + assert check_call("ps -xa | grep wazuh-syscheckd | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_remoted(): + assert check_call("ps -xa | grep wazuh-remoted | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_logcollec(): + assert check_call("ps -xa | grep wazuh-logcollec | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_monitord(): + assert check_call("ps -xa | grep wazuh-monitord | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_modulesd(): + assert check_call("ps -xa | grep wazuh-modulesd | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_wazuh_manager_apid(): + assert check_call("ps -xa | grep wazuh_apid | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh_cluster +def test_check_wazuh_manager_clusterd(): + assert check_call("ps -xa | grep clusterd.py | grep -v grep", shell=True) != "" + +@pytest.mark.wazuh +def test_check_filebeat_process(): + assert check_call("ps -xa | grep \"/usr/share/filebeat/bin/filebeat\" | grep -v grep", shell=True) != "" + +@pytest.mark.indexer +def test_check_indexer_process(): + assert check_call("ps -xa | grep wazuh-indexer | grep -v grep | cut -d \" \" -f15", shell=True) != "" + +@pytest.mark.dashboard +def test_check_dashboard_process(): + assert check_call("ps -xa | grep wazuh-dashboard | grep -v grep", shell=True) != "" + +@pytest.mark.indexer +def test_check_indexer_cluster_status_not_red(): + assert get_indexer_cluster_status() != "red" + +@pytest.mark.indexer_cluster +def test_check_indexer_cluster_status_not_yellow(): + assert get_indexer_cluster_status() != "yellow" + +@pytest.mark.dashboard +def test_check_dashboard_status(): + assert get_dashboard_status() == 200 + +@pytest.mark.wazuh +def test_check_wazuh_api_status(): + assert get_wazuh_api_status() == "Wazuh API REST" + +@pytest.mark.wazuh +def test_check_log_errors(): + found_error = False + exceptions = [ + 'WARNING: Cluster error detected', + 'agent-upgrade: ERROR: (8123): There has been an error executing the request in the tasks manager.', + "ERROR: Could not send message through the cluster after '10' attempts" + + ] + + with open('/var/ossec/logs/ossec.log', 'r') as f: + for line in f.readlines(): + if 'ERROR' in line: + if not any(exception in line for exception in exceptions): + found_error = True + break + assert found_error == False, line + +@pytest.mark.wazuh_cluster +def test_check_cluster_log_errors(): + found_error = False + with open('/var/ossec/logs/cluster.log', 'r') as f: + for line in f.readlines(): + if 'ERROR' in line: + found_error = True + break + assert found_error == False, line + +@pytest.mark.wazuh_worker +def test_check_cluster_log_errors(): + found_error = False + with open('/var/ossec/logs/cluster.log', 'r') as f: + for line in f.readlines(): + if 'ERROR' in line: + if 'Could not connect to master' not in line and 'Worker node is not connected to master' not in line and 'Connection reset by peer' not in line and "Error sending sendsync response to local client: Error 3020 - Timeout sending" not in line: + found_error = True + break + assert found_error == False, line + +@pytest.mark.wazuh_cluster +def test_check_api_log_errors(): + found_error = False + with open('/var/ossec/logs/api.log', 'r') as f: + for line in f.readlines(): + if 'ERROR' in line: + found_error = True + break + assert found_error == False, line + +@pytest.mark.indexer +def test_check_alerts(): + node_name = socket.gethostname() + query = { + "query": { + "bool": { + "must": [ + { + "wildcard": { + "agent.name": { + "value": '*' + } + } + } + ] + } + } + } + + response = api_call_indexer(get_indexer_ip(),query,get_indexer_ip(),'https',"admin",get_password("admin"),'9200') + + print(response) + + assert (response["hits"]["total"]["value"] > 0) diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 0000000..a41be7b --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,42 @@ +# Unit Test Instructions for Wazuh installation assistant + +This document provides instructions on how to run unit tests for the Wazuh installation assistant using Docker. + +## Overview + +- **Test Naming Convention**: All test files follow the naming pattern `tests-{script_name}.sh`, where `{script_name}.sh` corresponds to the script being tested. +- **Testing Environment**: The `unit-tests.sh` script is used to run these tests. It creates a clean Docker environment for each test run to ensure consistency. +- **Docker Requirement**: Docker must be installed, running, and accessible by the user. The Docker image used for testing is retained after the script execution to save time on subsequent runs. If the Dockerfile is modified, use the `-r` option to rebuild the image. + +## Usage + +``` +unit-tests.sh - Unit test for the Wazuh installation assistant. +``` + +### Synopsis + +``` +bash unit-tests.sh [OPTIONS] -a | -d | -f +``` + +### Options + +| Option | Description | +|-------------------------------------|-------------------------------------------------------------------| +| `-a`, `--test-all` | Runs tests on all available scripts. | +| `-d`, `--debug` | Displays the complete installation output for debugging purposes. | +| `-f`, `--files ` | Specifies a list of files to test. Example: `-f common checks`. | +| `-h`, `--help` | Displays the help message with usage details. | +| `-r`, `--rebuild-image` | Forces the Docker image to be rebuilt before running tests. | + +## Tips for Debugging + +When multiple tests fail after a merge, it can be challenging to isolate and fix them. Here's a method to streamline this process: + +> [!TIP] +> **1. Sequential Testing**: Since a bash script exits on an unknown character, you can insert a `Ç` character after the first test you want to run. Only the tests before the `Ç` character will be executed. +> +> **2. Incremental Fixing**: As you fix each test, move the `Ç` character down to include the next test or group of tests. This approach prevents you from having to scroll through all tests to identify which ones are failing. + +This technique allows for a more manageable and systematic approach to resolving issues, especially when dealing with a large number of tests. \ No newline at end of file diff --git a/tests/unit/docker-unit-testing-tool/Dockerfile b/tests/unit/docker-unit-testing-tool/Dockerfile new file mode 100644 index 0000000..03eeabd --- /dev/null +++ b/tests/unit/docker-unit-testing-tool/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:latest +RUN apk add --no-cache bash coreutils diffutils +RUN mkdir -p /tests/ + +COPY entrypoint.sh /usr/local/bin/test_file +RUN chmod +x /usr/local/bin/test_file + +# Set the entrypoint +ENTRYPOINT ["/usr/local/bin/test_file"] \ No newline at end of file diff --git a/tests/unit/docker-unit-testing-tool/entrypoint.sh b/tests/unit/docker-unit-testing-tool/entrypoint.sh new file mode 100644 index 0000000..9659b58 --- /dev/null +++ b/tests/unit/docker-unit-testing-tool/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +FILE_NAME=${1} +cd /tests/ +if [ -f test-${FILE_NAME}.sh ]; then + bash test-${FILE_NAME}.sh +else + echo "Couldn't find test-${FILE_NAME}.sh" +fi \ No newline at end of file diff --git a/tests/unit/framework/bach.sh b/tests/unit/framework/bach.sh new file mode 100644 index 0000000..6462d1e --- /dev/null +++ b/tests/unit/framework/bach.sh @@ -0,0 +1,642 @@ +# -*- mode: sh -*- +# Bach Testing Framework, https://bach.sh +# Copyright (C) 2019 Chai Feng +# +# Bach Testing Framework is dual licensed under: +# - GNU General Public License v3.0 +# - Mozilla Public License 2.0 +set -euo pipefail +shopt -s expand_aliases + +export BACH_COLOR="${BACH_COLOR:-auto}" +export PS4='+(${BASH_SOURCE##*/}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' + +BACH_OS_NAME="$(uname)" +declare -gxr BACH_OS_NAME + +declare -gxa bach_origin_paths=() +while builtin read -r -d: folder; do + bach_origin_paths+=("$folder") +done <<< "${PATH}:" + +function @out() { + if [[ "${1:-}" == "-" || ! -t 0 ]]; then + [[ "${1:-}" == "-" ]] && shift + while IFS=$'\n' read -r line; do + builtin printf "%s\n" "${*}$line" + done + elif [[ "$#" -gt 0 ]]; then + builtin printf "%s\n" "$*" + else + builtin printf "\n" + fi +} 8>/dev/null +export -f @out + +function @err() { + @out "$@" +} >&2 +export -f @err + +function @die() { + @out "$@" + exit 1 +} >&2 +export -f @die + +if [[ -z "${BASH_VERSION:-}" ]] || [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then + @die "Bach Testing Framework only support Bash v4+!" +fi + +if [[ "${BACH_DEBUG:-}" != true ]]; then + function @debug() { + : + } +else + exec 8>&2 + function @debug() { + builtin printf '[DEBUG] %s\n' "$*" + } >&8 +fi +export -f @debug + +function bach-real-path() { + declare folder name="$1" + declare altname="${name#*|}" + name="${name%|*}" + for folder in "${bach_origin_paths[@]}"; do + if [[ -x "$folder/$name" ]]; then + builtin echo "$folder/$name" + return 0 + elif [[ "$name" != "$altname" && -x "$folder/$altname" ]]; then + builtin echo "$folder/$altname" + return 0 + fi + done + return 1 +} +export -f bach-real-path + +export BACH_DEV_STDIN="" + +function bach_restore_stdin() { + if [[ ! -t 0 ]]; then + declare name + [[ -n "$BACH_DEV_STDIN" ]] || for name in /dev/ptmx /dev/pts/ptmx /dev/ttyv[0-9a-f]; do + if [[ -r "$name" && -c "$name" ]]; then + ls -l "$name" >&2 + BACH_DEV_STDIN="$name" + break + fi + done + exec 0<&- + exec 0<"$BACH_DEV_STDIN" + fi +} + +function bach_initialize(){ + enable -n alias bg bind dirs disown fc fg hash help history jobs kill suspend times ulimit umask unalias wait + + declare util name util_path + + declare -a bash_builtin_cmds=(cd echo enable popd pushd pwd shopt test trap type) + + for name in . command exec false set true unset "${bash_builtin_cmds[@]}"; do + eval "function @${name}() { builtin $name \"\$@\"; } 8>/dev/null; export -f @${name}" + done + + for name in eval; do + eval "function @${name}() { builtin $name \"\$@\"; }; export -f @${name}" + done + + function @source() { + declare script="$1" + shift + builtin source "$script" "$@" + } + + declare -a bach_core_utils=(cat chmod cut diff find env grep ls "shasum|sha1sum" mkdir mktemp rm rmdir sed sort tee touch which xargs) + + for util in "${bach_core_utils[@]}"; do + if [[ "$util" == "shasum|"* && "$BACH_OS_NAME" == FreeBSD ]]; then + util="shasum|sha1" + fi + name="${util%|*}" + util_path="$(bach-real-path "$util")" + eval "[[ -n \"${util_path}\" ]] || @die \"Fatal, CAN NOT find '$name' in \\\$PATH\"; function @${name}() { \"${util_path}\" \"\$@\"; } 8>/dev/null; export -f @${name}" + done + + bach_restore_stdin + @mockall "${bash_builtin_cmds[@]}" source . + + eval "$(builtin export | while read -rs name; do + name="${name%%=*}" + name="${name##* }" + [[ "${name^^}" != BACH_* ]] || continue + builtin echo "unset '$name' || builtin true" + done)" + builtin export LANG=C TERM=vt100 + unset name util_path +} + +function @real() { + declare name="$1" real_cmd + if [[ "$name" == */* ]]; then + @echo "$@" + return + fi + real_cmd="$(bach-real-path "$1" 7>&1)" + if [[ -z "${real_cmd}" ]]; then + real_cmd="${name}_not_found" + fi + declare -a cmd=("${real_cmd}" "${@:2}") + @debug "[REAL-CMD]" "${cmd[@]}" + "${cmd[@]}" +} +export -f @real + +function bach-get-all-functions() { + declare -F +} +export -f bach-get-all-functions + +function bach--skip-the-test() { + declare test="$1" test_filter + while read -d, test_filter; do + [[ -n "$test_filter" ]] || continue + [[ "$test" == $test_filter ]] && return 0 + [[ "$test" == test-$test_filter ]] && return 0 + done <<< "${BACH_TESTS:-}," +} +export -f bach--skip-the-test + +function bach-run-tests--get-all-tests() { + bach-get-all-functions | @sort -R | while read -r _ _ name; do + [[ "$name" == test?* ]] || continue + [[ "$name" == *-assert ]] && continue + bach--skip-the-test "$name" || continue + builtin printf "%s\n" "$name" + done +} + +for donotpanic in donotpanic dontpanic do-not-panic dont-panic do_not_panic dont_panic; do + eval "function @${donotpanic}() { builtin printf '\n%s\n line number: %s\n script stack: %s\n\n' 'DO NOT PANIC!' \"\${BASH_LINENO}\" \"\${BASH_SOURCE[*]}\"; builtin exit 1; } >&2; export -f @${donotpanic};" +done + +function bach--is-function() { + [[ "$(@type -t "$1")" == function ]] +} +export -f bach--is-function + +declare -gr __bach_run_test__ignore_prefix="## BACH:" +function @comment() { + @out "${__bach_run_test__ignore_prefix}" "$@" +} +export -f @comment + +function bach-run-tests() { + set -euo pipefail + + bach_initialize + + for donotpanic in donotpanic dontpanic do-not-panic dont-panic do_not_panic dont_panic; do + eval "function @${donotpanic}() { builtin true; }; export -f @${donotpanic}" + done + + function command() { + if [[ "$1" != -* ]] && bach--is-function "$1"; then + "$@" + else + mockfunc="$(@generate_mock_function_name command "$@")" + if bach--is-function "${mockfunc}"; then + @debug "[BC-func]" "${mockfunc}" "$@" + "${mockfunc}" "$@" + else + command_not_found_handle command "$@" + fi + fi + } + export -f command + + function xargs() { + declare param + declare -a xargs_opts + while param="${1:-}"; [[ -n "${param:-}" ]]; do + shift || true + if [[ "$param" == "--" ]]; then + xargs_opts+=("${BASH:-bash}" "-c" "$(builtin printf "'%s' " "$@") \$@" "-s") + break + else + xargs_opts+=("$param") + fi + done + @debug "@mock-xargs" "${xargs_opts[@]}" + if [[ "$#" -gt 0 ]]; then + @xargs "${xargs_opts[@]}" + else + [[ -t 0 ]] || @cat &>/dev/null + @dryrun xargs "${xargs_opts[@]}" + fi + } + export -f xargs + + if [[ "${BACH_ASSERT_IGNORE_COMMENT}" == true ]]; then + BACH_ASSERT_DIFF_OPTS+=(-I "^${__bach_run_test__ignore_prefix}") + fi + + declare color_ok color_err color_end + if [[ "$BACH_COLOR" == "always" ]] || [[ "$BACH_COLOR" != "no" && -t 1 && -t 2 ]]; then + color_ok="\e[1;32m" + color_err="\e[1;31m" + color_end="\e[0;m" + else + color_ok="" + color_err="" + color_end="" + fi + declare name friendly_name testresult test_name_assert_fail + declare -i total=0 error=0 + declare -a all_tests + mapfile -t all_tests < <(bach-run-tests--get-all-tests) + @echo "1..${#all_tests[@]}" + for name in "${all_tests[@]}"; do + # @debug "Running test: $name" + friendly_name="${name/#test-/}" + friendly_name="${friendly_name//-/ }" + friendly_name="${friendly_name// / -}" + : $(( ++total )) + testresult="$(@mktemp)" + @set +e + assert-execution "$name" &>"$testresult"; test_retval="$?" + @set -e + if [[ "$name" == test-ASSERT-FAIL-* ]]; then + test_retval="$(( test_retval == 0?1:0 ))" + test_name_assert_fail="${color_err}ASSERT FAIL${color_end}" + friendly_name="${friendly_name/#ASSERT FAIL/}" + else + test_name_assert_fail="" + fi + if [[ "$test_retval" -eq 0 ]]; then + builtin printf "${color_ok}ok %d - ${test_name_assert_fail}${color_ok}%s${color_end}\n" "$total" "$friendly_name" + else + : $(( ++error )) + builtin printf "${color_err}not ok %d - ${test_name_assert_fail}${color_err}%s${color_end}\n" "$total" "$friendly_name" + { + builtin printf "\n" + @cat "$testresult" >&2 + builtin printf "\n" + } >&2 + fi + @rm "$testresult" &>/dev/null + done + + declare color_result="$color_ok" + if (( error > 0 )); then + color_result="$color_err" + fi + builtin printf -- "# -----\n#${color_result} All tests: %s, failed: %d, skipped: %d${color_end}\n" \ + "${#all_tests[@]}" "$error" "$(( ${#all_tests[@]} - total ))">&2 + [[ "$error" == 0 ]] && [[ "${#all_tests[@]}" -eq "$total" ]] +} + +function bach-on-exit() { + if [[ -o xtrace ]]; then + exec 8>&2 + BASH_XTRACEFD=8 + fi + if [[ "$?" -eq 0 ]]; then + [[ "${BACH_DISABLED:-false}" == true ]] || bach-run-tests + else + builtin printf "Bail out! %s\n" "Couldn't initlize tests." + fi +} + +trap bach-on-exit EXIT + +function @generate_mock_function_name() { + declare name="$1" + @echo "mock_exec_${name}_$(@dryrun "${@}" | @shasum | @cut -b1-7)" +} +export -f @generate_mock_function_name + +function @mock() { + declare -a param name cmd func body desttype + name="$1" + if [[ "$name" == @(builtin|declare|eval|set|unset|true|false|read) ]]; then + @die "Cannot mock the builtin command: $name" + fi + if [[ command == "$name" && "$2" != -* ]]; then + shift + name="$1" + fi + desttype="$(@type -t "$name" || true)" + for param; do + if [[ "$param" == '===' ]]; then + shift + break + fi + cmd+=("$param") + done + shift "${#cmd[@]}" + if [[ "$name" == /* ]]; then + @die "Cannot mock an absolute path: $name" + elif [[ "$name" == */* ]] && [[ -e "$name" ]]; then + @die "Cannot mock an existed path: $name" + fi + @debug "@mock $name" + if [[ "$#" -gt 0 ]]; then + @debug "@mock $name $*" + declare -a params=("$@") + func="$(declare -p params); \"\${params[@]}\"" + #func="$*" + elif [[ ! -t 0 ]]; then + @debug "@mock $name @cat" + func="$(@cat)" + fi + if [[ -z "${func:-}" ]]; then + @debug "@mock default $name" + func="if [[ -t 0 ]]; then @dryrun \"${name}\" \"\$@\" >&7; else @cat; fi" + fi + if [[ "$name" == */* ]]; then + [[ -d "${name%/*}" ]] || @mkdir -p "${name%/*}" + @cat > "$name" <