Full Code of mandiant/flare-vm for AI

main d7fcee7563d4 cached
26 files
195.5 KB
49.4k tokens
39 symbols
1 requests
Download .txt
Showing preview only (205K chars total). Download the full file or copy to clipboard to get everything.
Repository: mandiant/flare-vm
Branch: main
Commit: d7fcee7563d4
Files: 26
Total size: 195.5 KB

Directory structure:
gitextract_n6_o4m_t/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug.yml
│   │   ├── config.yml
│   │   └── feature.yml
│   └── workflows/
│       ├── build-vbox.yaml
│       └── linter.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE.txt
├── LayoutModification.xml
├── README.md
├── config.xml
├── install.ps1
├── scripts/
│   └── lint.ps1
└── virtualbox/
    ├── README.md
    ├── configs/
    │   ├── remnux.yaml
    │   ├── win10_flare-vm-edu.yaml
    │   ├── win10_flare-vm.yaml
    │   └── win11_flare-vm.yaml
    ├── install.sh
    ├── vbox-adapter-check.py
    ├── vbox-build-flare-vm.py
    ├── vbox-build-remnux.py
    ├── vbox-clean-snapshots.py
    ├── vbox-export-snapshot.py
    └── vboxcommon.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto

# Explicitly declare text files we want to always be normalized and converted
# to native line endings on checkout.
*.md            text
*.gitattributes text

# Declare files that will always have CRLF line endings on checkout.
*.ps1    text  eol=crlf
*.psm1   text  eol=crlf
*.psd1   text  eol=crlf
*.psc1   text  eol=crlf
*.ps1xml text  eol=crlf
*.clixml text  eol=crlf
*.xml    text  eol=crlf
*.txt    text  eol=crlf
*.nuspec text  eol=crlf
*.reg    text  eol=crlf


================================================
FILE: .github/ISSUE_TEMPLATE/bug.yml
================================================
name: 🐛 Bug
description: You need help installing FLARE-VM or something doesn't work as expected
labels: [":bug: bug"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for helping improving FLARE-VM. Before submitting your issue:
        - Read the [Troubleshooting section in the README](https://github.com/mandiant/flare-vm#troubleshooting).
        - We track only bugs related to the installer in this repository. If the issue is related to a concrete tool or package (for example a single package that fails to install), please report it in [VM-Packages](https://github.com/mandiant/VM-Packages/issues/new?assignees=&labels=%3Abug%3A+bug&template=bug.yml).
        - Check the [open issues](https://github.com/mandiant/flare-vm/issues) and ensure there is not already a similar issue. If there is already a similar issue, please add more details there instead of opening a new one.
        - Ensure you are running the [latest version of the FLARE-VM installer](https://github.com/mandiant/flare-vm/blob/main/install.ps1).
        - Ensure your VM satisfies the [requirements](https://github.com/mandiant/flare-vm#requirements) such as having internet connection.
        - Fill all the requested information accurately in this issue to ensure we are able to help you.
        - If you know how to solve this problem, please send also a pull request! :pray:
  - type: textarea
    id: problem
    attributes:
      label: What's the problem?
      description: Include the actual and expected behavior. The more details, the better!
    validations:
      required: true
  - type: textarea
    id: steps
    attributes:
      label: Steps to Reproduce
      placeholder: |
        1. First Step
        2. Second Step
        3. and so on…
    validations:
      required: true
  - type: textarea
    id: environment
    attributes:
      label: Environment
      description: |
        Include the following details about your environment:
        - **Virtualization software**: VMWare, VirtualBox, etc.
        - **VM OS version**: run `(Get-CimInstance Win32_OperatingSystem).version` in Powershell
        - **VM PowerShell version**: run `$PSVersionTable.PSVersion.ToString()` in Powershell
        - **VM Chocolatey version**: run `choco --version`
        - **VM Boxstarter version**: run `choco info -l -r "boxstarter"`
        - **Output of `VM-Get-Host-Info`** that will be available if the `vm.common` package has been install: run `VM-Get-Host-Info` in PowerShell with admin rights
      placeholder: |
        - Virtualization software: 
        - VM OS version: 
        - VM PowerShell version: 
        - VM Chocolatey version: 
        - VM Boxstarter version: 
        - Output of `VM-Get-Host-Info`: 

    validations:
      required: true
  - type: textarea
    id: extra-info
    attributes:
      label: Additional Information
      description: |
        Any additional information, configuration or data that might be necessary to understand and reproduce the issue. For example:
        - Console output
        - The log files `C:\ProgramData\_VM\log.txt` and `C:\ProgramData\chocolatey\logs\chocolatey.log`

        Text logs are preferred over screenshots.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false


================================================
FILE: .github/ISSUE_TEMPLATE/feature.yml
================================================
name: 💡 Feature proposal
description: Propose a new feature or improvement.
body:
  - type: markdown
    attributes:
      value: |
        Thanks for helping improving FLARE-VM. Before submitting your issue:
        - **If you need help installing FLARE-VM or want to report a bug, use the [bug issue type](https://github.com/mandiant/flare-vm/issues/new?assignees=&labels=%3Abug%3A+bug&template=bug.yml) instead and provide all the information requested there.** Otherwise we won't be able to help you.
        - We track only features related to the installer in this repository. If the issue is related to a concrete tool or package, please report it in [VM-Packages](https://github.com/mandiant/VM-Packages/issues/new).
        - Check the [open issues](https://github.com/mandiant/flare-vm/issues) and ensure there is not already a similar issue. If there is already a similar issue, please add more details there instead of opening a new one.
  - type: textarea
    id: problem
    attributes:
      label: Details
      description: The more details, the better!
    validations:
      required: true


================================================
FILE: .github/workflows/build-vbox.yaml
================================================
name: Build & release vbox
# Create or update a release by adding a body and the built vbox binaries on a tag creation that starts by `vbox-`.
# The tag can be created in the GH UI (by creating a release) or using git, e.g.:
# VERSION=1.0.0 && git tag -a vbox-$VERSION origin/main -m "vbox tag" && git push origin vbox-$VERSION

on:
  push:
    tags:
      - 'vbox-*'
  workflow_dispatch: # manual trigger for testing

permissions:
  contents: write

jobs:
  build:
    # use old linux for better portability
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - name: Install build requirements
        run: python -m pip install --upgrade pip pyinstaller
      - name: Build standalone executables
        run: |
          cd virtualbox

          pyinstaller --onefile --log-level DEBUG vbox-adapter-check.py
          pyinstaller --onefile --log-level DEBUG vbox-clean-snapshots.py

          ls dist
      # Only test vbox-clean-snapshots as vbox-adapter-check uses Notify and it fails in GH actions
      - name: Check  vbox-clean-snapshots runs correctly
        run: virtualbox/dist/vbox-clean-snapshots --help
      - name: Make files executable
        run: |
          chmod +x virtualbox/dist/*
          chmod +x virtualbox/install.sh
      - name: Zip binaries
        run: zip -rj vbox.zip virtualbox/dist virtualbox/install.sh
      - name: Upload ZIP to release
        uses: svenstaro/upload-release-action@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # v2.11.2
        with:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          file: vbox.zip
          tag: ${{ github.ref }}
          overwrite: true
          body: |
            **FLARE-VM does not have releases**, as the project relies on external URLs outside of our control, making it impossible to install an older version. This release only includes standalone Linux executables for some of the scripts in the [`virtualbox`](https://github.com/mandiant/flare-vm/tree/main/virtualbox) folder (related to use FLARE-VM in VirtualBox) and a bash script to make it easier to use them. **To install FLARE-VM check the instructions in the [FLARE-VM README](https://github.com/mandiant/flare-vm)**.

            - **vbox-adapter-check**: Print the status of all internet adapters of all VMs in VirtualBox. Useful to detect internet access, which is undesirable for dynamic malware analysis. Compatibility is limited to Linux systems using the GTK graphical toolkit, which includes desktops like GNOME.
            - **vbox-clean-snapshots**: Delete a snapshot and its children recursively skipping snapshots with a substring in the name. Useful to delete several snapshots, which is not possible via the VirtualBox UI.



================================================
FILE: .github/workflows/linter.yml
================================================
name: Linter

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  lint:
    runs-on: windows-2022
    steps:
      - name: Checkout code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - name: Install dependencies
        run: pip install black==25.* isort==6.* flake8==7.*
      # Different line limits: Black/isort (120), Flake8 (150). 
      # Flake8 allows longer lines for better long string readability. Black doesn't enforce string length.
      - name: Run black
        run: black --line-length=120 --check --diff .
      - name: Run flake8
        run: flake8 --max-line-length=150
      - name: Run isort
        run: isort --check --diff --profile black --line-length=120 .
      - name: Run PowerShell linter
        run: scripts/lint.ps1




================================================
FILE: .gitignore
================================================
# Compiled source #
###################
*.com
*.class
*.o
*.so
__pycache__/

# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.rar
*.tar
*.nupkg

# Logs and databases #
######################
*.log
*.sql
*.sqlite

# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Pycharm artifacts
###################
.idea

# vscode
# #################
.vscode/


================================================
FILE: CONTRIBUTING.md
================================================
# How to contribute

Want to open an issue or send a code contribution?
Read the information below to learn how.
We are looking forward working with you to improve FLARE-VM! :sparkling_heart:

## Repository structure of FLARE-VM

The FLARE-VM code is spited in two repositories:
- **[FLARE-VM](https://github.com/mandiant/flare-vm) (this repository)**: FLARE-VM installation script, and configuration
  - [Submit improvement proposals and report issues related to the installer](https://github.com/mandiant/flare-vm/issues/new/choose)

- **[VM-Packages](https://github.com/mandiant/VM-Packages)**: Source code of tool packages used by FLARE-VM (this repository) and [CommandoVM](https://github.com/mandiant/commando-vm)
  - [Documentation and contribution guides for tool packages](https://github.com/mandiant/VM-Packages/wiki)
  - [Submit new tool packages or report package related issues](https://github.com/mandiant/VM-Packages/issues/new/choose)

Before opening an issue, ensure you select the correct repository ([FLARE-VM](https://github.com/mandiant/flare-vm) for the FLARE-VM installer, [VM-Packages](https://github.com/mandiant/VM-Packages) for concrete tools and packages).
Select the correct issue type and read the issue template carefully to ensure you provide all needed information.

## Before contributing code

### Sign our Contributor License Agreement

Contributions to this project must be accompanied by a [Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project.

If you or your current employer have already signed the Google CLA (even if it was for a different project), you probably don't need to do it again.

Visit <https://cla.developers.google.com/> to see your current agreements or to sign a new one.

## Review our community guidelines

This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct).


================================================
FILE: LICENSE.txt
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: LayoutModification.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
 Copyright 2023 Google LLC

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-->

<LayoutModificationTemplate
    xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
    xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout"
    xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout"
    xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout"
    Version="1">
  <LayoutOptions StartTileGroupCellWidth="6" StartTileGroupsColumnCount="1" />
  <DefaultLayoutOverride>
    <StartLayoutCollection>
      <defaultlayout:StartLayout GroupCellWidth="6">
      </defaultlayout:StartLayout>
    </StartLayoutCollection>
  </DefaultLayoutOverride>
    <CustomTaskbarLayoutCollection PinListPlacement="Replace">
      <defaultlayout:TaskbarLayout>
        <taskbar:TaskbarPinList>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%AppData%\Microsoft\Windows\Start Menu\Programs\System Tools\File Explorer.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Productivity Tools\Windows Terminal.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Utilities\CyberChef.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Disassemblers\ida.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Networking\fakenet.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\PE\CFF Explorer.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Utilities\procexp.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Utilities\procmon.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Productivity Tools\notepad++.lnk"/>
          <taskbar:DesktopApp DesktopApplicationLinkPath="%TOOL_LIST_DIR%\Productivity Tools\VisualStudio.lnk"/>
        </taskbar:TaskbarPinList>
      </defaultlayout:TaskbarLayout>
    </CustomTaskbarLayoutCollection>
</LayoutModificationTemplate>


================================================
FILE: README.md
================================================
# FLARE-VM
Welcome to FLARE-VM - a collection of software installations scripts for Windows systems that allows you to easily setup and maintain a reverse engineering environment on a virtual machine (VM). FLARE-VM was designed to solve the problem of reverse engineering tool curation and relies on two main technologies: [Chocolatey](https://chocolatey.org) and [Boxstarter](https://boxstarter.org). Chocolatey is a Windows-based Nuget package management system, where a "package" is essentially a ZIP file containing PowerShell installation scripts that download and configure a specific tool. Boxstarter leverages Chocolatey packages to automate the installation of software and create repeatable, scripted Windows environments.

<p align="center">
  <img src="Images/flarevm-logo.png" alt="FLARE-VM Logo" width="600">
</p>

## Requirements
**FLARE-VM should ONLY be installed on a virtual machine**.
The VM should satisfy the following requirements:

* Windows ≥ 10
* PowerShell ≥ 5
* Disk capacity of at least 60 GB and memory of at least 2GB
* Usernames without spaces or other special characters
* Internet connection
* Tamper Protection and any Anti-Malware solution (e.g., Windows Defender) disabled, preferably via Group Policy
* Windows Updates Disabled

## Installation instruction
This section documents the steps to install FLARE-VM. You may also find useful the [_Building a VM for Reverse Engineering and Malware Analysis! Installing the FLARE-VM_ video](https://www.youtube.com/watch?v=i8dCyy8WMKY).

### Pre-installation
* Prepare a Windows 10+ virtual machine
  * Install Windows in the virtual machine, for example using the raw Windows 10 ISO from https://www.microsoft.com/en-us/software-download/windows10ISO
  * Ensure the [requirements above](#requirements) are satisfied, including:
    * Disable Windows Updates (at least until installation is finished)
      * https://www.windowscentral.com/how-stop-updates-installing-automatically-windows-10
    * Disable Tamper Protection and any Anti-Malware solution (e.g., Windows Defender), preferably via Group Policy.
      * GPO: [https://stackoverflow.com/questions/62174426/how-to-permanently-disable-windows-defender-real-time-protection-with-gpo](https://superuser.com/a/1757341)
      * Non-GPO - Manual: [https://www.maketecheasier.com/permanently-disable-windows-defender-windows-10/](https://www.maketecheasier.com/permanently-disable-windows-defender-windows-10)
      * Non-GPO - Automated: [https://github.com/ionuttbara/windows-defender-remover](https://github.com/ionuttbara/windows-defender-remover)
      * Non-GPO - Semi-Automated (User needs to toggle off Tamper Protection): [https://github.com/AveYo/LeanAndMean/blob/main/ToggleDefender.ps1](https://github.com/AveYo/LeanAndMean/blob/main/ToggleDefender.ps1)
* Take a VM snapshot so you can always revert to a state before the FLARE-VM installation
* NOTE for IDA Pro: If you are installing IDA Pro via `idapro.vm`, you must place your IDA Pro installer (and optionally, your license file) on the Desktop before running the FLARE-VM installer.

### FLARE-VM installation
* Open a `PowerShell` prompt as administrator
* Download the installation script [`installer.ps1`](https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1) to your Desktop:
  * `(New-Object net.webclient).DownloadFile('https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1',"$([Environment]::GetFolderPath("Desktop"))\install.ps1")`
* Unblock the installation script:
  * `Unblock-File .\install.ps1`
* Enable script execution:
  * `Set-ExecutionPolicy Unrestricted -Force`
    * If you receive an error saying the execution policy is overridden by a policy defined at a more specific scope, you may need to pass a scope in via `Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force`. To view execution policies for all scopes, execute `Get-ExecutionPolicy -List`
* Finally, execute the installer script as follow:
  * `.\install.ps1`
    * To pass your password as an argument: `.\install.ps1 -password <password>`
    * To use the CLI-only mode with minimal user interaction: `.\install.ps1 -password <password> -noWait -noGui`
    * To use the CLI-only mode with minimal user interaction and a custom config file: `.\install.ps1 -customConfig <config.xml> -password <password> -noWait -noGui`
* After installation it is recommended to switch to `host-only` networking mode and take a VM snapshot

#### Installer Parameters
Below are the CLI parameter descriptions.

```
PARAMETERS
    -password <String>
        Current user password to allow reboot resiliency via Boxstarter. The script prompts for the password if not provided.

    -noPassword [<SwitchParameter>]
        Switch parameter indicating a password is not needed for reboots.

    -customConfig <String>
        Path to a configuration XML file. May be a file path or URL.

    -customLayout <String>
        Path to a taskbar layout XML file. May be a file path or URL.

    -noWait [<SwitchParameter>]
        Switch parameter to skip installation message before installation begins.

    -noGui [<SwitchParameter>]
        Switch parameter to skip customization GUI.

    -noReboots [<SwitchParameter>]
        Switch parameter to prevent reboots (not recommended).

    -noChecks [<SwitchParameter>]
        Switch parameter to skip validation checks (not recommended).
```

Get full usage information by running `Get-Help .\install.ps1 -Detailed`.

#### Installer GUI

The Installer GUI is display after executing the validation checks and installing Boxstarter and Chocolatey (if they are not installed already).
Using the installer GUI you may customize:
* Package selection from FLARE-VM and Chocolatey community
* Environment variable paths

![Installer GUI](Images/installer-gui.png)

#### Configuration

The installer will download [`config.xml`](https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml) from the FLARE-VM repository. This file contains the default configuration, including the list of packages to install and the environment variable paths. You may use your own configuration by specifying the CLI-argument `-customConfig` and providing either a local file path or URL to your `config.xml` file. For example:

```
.\install.ps1 -customConfig "https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml"
```

#### Taskbar Layout
The installer will use [`CustomStartLayout.xml`](https://raw.githubusercontent.com/mandiant/flare-vm/main/CustomStartLayout.xml) from the FLARE-VM repository. This file contains the default taskbar layout. You may use your own configuration by specifying the CLI-argument `-customLayout` and providing a local file path or URL to your `CustomStartLayout.xml` file. For example:

```
.\install.ps1 -customLayout "https://raw.githubusercontent.com/mandiant/flare-vm/main/CustomStartLayout.xml"
```

##### Things to Consider:
- Items in the .xml that are not installed will not display in the taskbar (no broken links will be pinned)
- Only applications (`.exe` files) or shortcuts to applications can be pinned.
- If you would like to pin something that isn't an application, consider creating a shortcut that points to `cmd.exe` or `powershell` with arguments supplied that will perform that actions you would like.
- If you would like to make something run with admin rights, consider making a shortcut using `VM-Install-Shortcut` with the flag `-runAsAdmin` and pinning the shortcut.


#### Post installation steps
You can include any post installation step you like in the configuration inside the tags `apps`, `services`, `path-items`, `registry-items`, and `custom-items`.

For example:
- To show known file extensions:
```xml
    <registry-items>
        <registry-item name="Show known file extensions" path="HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" value="HideFileExt" type="DWord" data="0"/>
    </registry-items>
```

For more examples, check the default configuration file: [`config.xml`](https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml).


## Contributing

- Check our [CONTRIBUTING guide](/CONTRIBUTING.md) to learn how to contribute to the project.

## Troubleshooting
If your installation fails, please attempt to identify the reason for the installation error by reading through the log files listed below on your system:
* `%VM_COMMON_DIR%\log.txt`
* `%PROGRAMDATA%\chocolatey\logs\chocolatey.log`
* `%LOCALAPPDATA%\Boxstarter\boxstarter.log`

Ensure you are running the latest version of the FLARE-VM installer and that your VM satisfies the [requirements](#requirements).

### Installer Error
If the installation failed due to an issue in the installation script (e.g., `install.ps1`), [report the bug in FLARE-VM](https://github.com/mandiant/flare-vm/issues/new?labels=%3Abug%3A+bug&template=bug.yml).
Provide all the information requested to ensure we are able to help you.

> **Note:** Rarely should `install.ps1` be the reason for an installation failure. Most likely it is a specific package or set of packages that are failing (see below).

### Package Error
Packages fail to install from time to time -- this is normal. The most common reasons are outlined below:

1. Failure or timeout from Chocolatey or MyGet to download a `.nupkg` file
2. Failure or timeout due to remote host when downloading a tool
3. Intrusion Detection System (IDS) or AV product (e.g., Windows Defender) prevents a tool download or removes the tool from the system
4. Host specific issue, for example when using an untested version
5. Tool fails to build due to dependencies
6. Old tool URL (e.g., `HTTP STATUS 404`)
7. Tool's SHA256 hash has changed from what is hardcoded in the package installation script

Reasons **1-4** are difficult for us to fix since we do not control them. If an issue related to reasons **1-4** is filed, it is unlikely we will be able to assist.

We can help with reasons **5-7** and welcome the community to contribute fixes as well!
Please [report the bug in VM-Packages](https://github.com/mandiant/VM-Packages/issues/new?labels=%3Abug%3A+bug&template=bug.yml) providing all the information requested.

### Updates

Note that package updates are best effort and that updates are not being tested.
If you encounter errors, perform a fresh FLARE-VM install.

### Mailing List
Subscribe to the FLARE mailing list for community announcements! Email "subscribe" to [flare-external@google.com](mailto:flare-external@google.com?subject=subscribe).

## Legal Notice
> This download configuration script is provided to assist cyber security analysts in creating handy and versatile toolboxes for malware analysis environments. It provides a convenient interface for them to obtain a useful set of analysis tools directly from their original sources. Installation and use of this script is subject to the Apache 2.0 License. You as a user of this script must review, accept and comply with the license terms of each downloaded/installed package. By proceeding with the installation, you are accepting the license terms of each package, and acknowledging that your use of each package will be subject to its respective license terms.


================================================
FILE: config.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
 Copyright 2017 Google LLC

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-->

<config>
    <envs>
        <env name="VM_COMMON_DIR" value="%ProgramData%\_VM"/>
        <env name="TOOL_LIST_DIR" value="%UserProfile%\Desktop\Tools"/>
        <env name="RAW_TOOLS_DIR" value="%SystemDrive%\Tools"/>
    </envs>
    <packages>
        <package name="010editor.vm"/>
        <package name="7zip.vm"/>
        <package name="advanced-installer.vm"/>
        <package name="angr.vm"/>
        <package name="apimonitor.vm"/>
        <package name="apktool.vm"/>
        <package name="asar.vm"/>
        <package name="autoit-ripper.vm"/>
        <package name="binaryninja.vm"/>
        <package name="bindiff.vm"/>
        <package name="blobrunner.vm"/>
        <package name="blobrunner64.vm"/>
        <package name="bytecodeviewer.vm"/>
        <package name="capa-explorer-web.vm"/>
        <package name="capa.vm"/>
        <package name="chrome.extensions.vm"/>
        <package name="cmder.vm"/>
        <package name="codetrack.vm"/>
        <package name="cryptotester.vm"/>
        <package name="cutter.vm"/>
        <package name="cyberchef.vm"/>
        <package name="cygwin.vm"/>
        <package name="de4dot-cex.vm"/>
        <package name="dependencywalker.vm"/>
        <package name="dex2jar.vm"/>
        <package name="didier-stevens-beta.vm"/>
        <package name="didier-stevens-suite.vm"/>
        <package name="die.vm"/>
        <package name="dll-to-exe.vm"/>
        <package name="dnlib.vm"/>
        <package name="dnspyex.vm"/>
        <package name="dotdumper.vm"/>
        <package name="dotnet3.5"/> <!-- To run old .NET binaries -->
        <package name="exeinfope.vm"/>
        <package name="explorersuite.vm"/>
        <package name="extreme_dumper.vm"/>
        <package name="ezviewer.vm"/>
        <package name="fakenet-ng.vm"/>
        <package name="file.vm"/>
        <package name="fiddler.vm"/>
        <package name="floss.vm"/>
        <package name="garbageman.vm"/>
        <package name="ghidra.vm"/>
        <package name="goresym.vm"/>
        <package name="gostringungarbler.vm"/>
        <package name="hashmyfiles.vm"/>
        <package name="hollowshunter.vm"/>
        <package name="hxd.vm"/>
        <package name="ida.plugin.capa.vm"/>
        <package name="ida.plugin.comida.vm"/>
        <package name="ida.plugin.delphihelper.vm"/>
        <package name="ida.plugin.dereferencing.vm"/>
        <package name="ida.plugin.diaphora.vm"/>
        <package name="ida.plugin.flare-emu.vm"/>
        <package name="ida.plugin.flare.vm"/>
        <package name="ida.plugin.hashdb.vm"/>
        <package name="ida.plugin.hrtng.vm"/>
        <package name="ida.plugin.ifl.vm"/>
        <package name="ida.plugin.xray.vm"/>
        <package name="ida.plugin.xrefer.vm"/>
        <package name="idafree.vm"/>
        <package name="idr.vm"/>
        <package name="ifpstools.vm"/>
        <package name="ilspy.vm"/>
        <package name="innoextract.vm"/>
        <package name="innounp.vm"/>
        <package name="internet_detector.vm"/>
        <package name="ipython.vm"/>
        <package name="isd.vm"/>
        <package name="js-beautify.vm"/>
        <package name="js-deobfuscator.vm"/>
        <package name="keystone.vm"/>
        <package name="libraries.python3.vm"/>
        <package name="magika.vm"/>
        <package name="malware-jail.vm"/>
        <package name="map.vm"/>
        <package name="microsoft-office.vm"/>
        <package name="nasm.vm"/>
        <package name="net-reactor-slayer.vm"/>
        <package name="nmap.vm"/>
        <package name="notepadplusplus.vm"/>
        <package name="notepadpp.plugin.compare.vm"/>
        <package name="notepadpp.plugin.jstool.vm"/>
        <package name="notepadpp.plugin.xmltools.vm"/>
        <package name="obfuscator-io-deobfuscator.vm"/>
        <package name="offvis.vm"/>
        <package name="onenoteanalyzer.vm"/>
        <package name="pdbresym.vm"/>
        <package name="pdfstreamdumper.vm"/>
        <package name="pe_unmapper.vm"/>
        <package name="pebear.vm"/>
        <package name="peid.vm"/>
        <package name="pesieve.vm"/>
        <package name="pestudio.vm"/>
        <package name="pkg-unpacker.vm"/>
        <package name="pma-labs.vm"/>
        <package name="procdot.vm"/>
        <package name="processdump.vm"/>
        <package name="psnotify.vm"/>
        <package name="pycdas.vm"/>
        <package name="pycdc.vm"/>
        <package name="pylingual.vm"/>
        <package name="rat-king-parser.vm"/>
        <package name="recaf.vm"/>
        <package name="reg_export.vm"/>
        <package name="regcool.vm"/>
        <package name="regshot.vm"/>
        <package name="resourcehacker.vm"/>
        <package name="rundotnetdll.vm"/>
        <package name="scdbg.vm"/>
        <package name="sclauncher.vm"/>
        <package name="sclauncher64.vm"/>
        <package name="sfextract.vm"/>
        <package name="shellcode_launcher.vm"/>
        <package name="sysinternals.vm"/>
        <package name="systeminformer.vm"/>
        <package name="ttd.vm"/>
        <package name="uncompyle6.vm"/>
        <package name="uniextract2.vm"/>
        <package name="unpyc3.vm"/>
        <package name="upx.vm"/>
        <package name="vb-decompiler-lite.vm"/>
        <package name="vbdec.vm"/>
        <package name="vcbuildtools.vm"/>
        <package name="vcredist-all"/>
        <package name="vscode.extension.jupyter.vm"/>
        <package name="vscode.extension.python.vm"/>
        <package name="vscode.vm"/>
        <package name="windbg.vm"/>
        <package name="windows-terminal.vm"/>
        <package name="wireshark.vm"/>
        <package name="x64dbg.plugin.dbgchild.vm"/>
        <package name="x64dbg.plugin.ollydumpex.vm"/>
        <package name="x64dbg.plugin.scyllahide.vm"/>
        <package name="x64dbg.plugin.x64dbgpy.vm"/>
        <package name="x64dbg.vm"/>
        <package name="yara.vm"/>
    </packages>
    <apps>
        <!--
        INFO:
        Removes installed AppX packages. Try:
        $packages = Get-AppxPackage
        $packages.Name
        FORMAT:
        <app name="APP_NAME"/>
        -->
    </apps>
    <services>
        <!--
        INFO:
        Sets Services to "Manual" startup type. Try:
        $services = Get-WmiObject -Query "SELECT * FROM Win32_Service WHERE StartMode='Auto'" | Get-Service
        $services.Name
        FORMAT:
        <service name="SERVICE_NAME"/>
        -->
    </services>
    <tasks>
        <!--
        INFO:
        Disables Scheduled Tasks. Try:
        $tasks = Get-ScheduledTask
        $tasks.TaskName
        FORMAT:
        <task name="DESCRIPTIVE_NAME" value="TASK_NAME"/>
        -->
    </tasks>
    <path-items>
        <!--
        INFO:
        Removes files and folders from the system
        FORMAT:
        <path-item name="DESCRIPTIVE_NAME" type="dir/file" path="DIR_PATH/FILE_PATH"/>
        -->
    </path-items>
    <registry-items>
        <!--
        INFO:
        Makes custom edits to the registry
        FORMAT:
        <registry-item name="DESCRIPTIVE_NAME" path="REG_PATH" value="REG_VALUE" type="TYPE" data="NEW_DATA"/>
        -->
        <registry-item name="Show full directory path in Explorer title bar" path="HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\CabinetState" value="FullPath" type="DWord" data="1"/>
        <registry-item name="Show known file extensions" path="HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" value="HideFileExt" type="DWord" data="0"/>
        <registry-item name="Show hidden files" path="HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" value="Hidden" type="DWord" data="1"/>
        <registry-item name="Disable SmartScreen" path="HKLM:\SOFTWARE\Policies\Microsoft\Windows\System" value="EnableSmartScreen" type="DWord" data="0" />
        <registry-item name="Disable Microsoft Edge Phishing Filter" path="HKLM:\SOFTWARE\Policies\Microsoft\MicrosoftEdge\PhishingFilter" value="EnabledV9" type="DWord" data="0" />
        <registry-item name="Disable Windows Firewall (Standard Profile)" path="HKLM:\SOFTWARE\Policies\Microsoft\WindowsFirewall\StandardProfile" value="EnableFirewall" type="DWord" data="0" />
        <registry-item name="Add ZoomIt to Windows Start" path="HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\run" value="ZoomIt" type="String" data="C:\Tools\sysinternals\ZoomIt64.exe" />
        <registry-item name="Don't display ZoomIt GUI on login" path="HKCU:\Software\Sysinternals\ZoomIt" value="OptionsShown" type="DWord" data="1" />
        <registry-item name="Hide the .lnk extension" path="HKLM:\SOFTWARE\Classes\lnkfile" value="NeverShowExt" type="String" data=" "/>
        <!-- Set dark mode
        <registry-item name="Set Dark Mode on System" path="HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" value="SystemUsesLightTheme" type="DWord" data="0"/>
        <registry-item name="Set Dark Mode on Apps" path="HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" value="AppsUseLightTheme" type="DWord" data="0"/>
        -->
    </registry-items>
    <locales>
        <!--
        INFO:
        Install extra system locales. Useful to analyse malware that behave differently depending on the locale.
        FORMAT:
        <locale name="DESCRIPTIVE_NAME" lang="LANGUAGE_VALUE"/>
        -->
        <locale name="Chinese" lang="zh-CN"/>
        <locale name="English (United Kingdom)" lang="en-GB"/>
        <locale name="German (Germany)" lang="de-DE"/>
        <locale name="Russian" lang="ru-RU"/>
        <locale name="Spanish (Spain)" lang="es-ES"/>
        <locale name="Ukrainian" lang="uk-UA"/>
        <locale name="Brazilian Portuguese" lang="pt-BR"/>
    </locales>
    <custom-items>
        <!--
        INFO:
        Performs custom commands
        FORMAT:
        <custom-item name="DESCRIPTIVE_NAME"> <cmd value="PS_COMMAND"/> ... </custom-item>
        -->
        <custom-item name="Disabling Windows Firewall">
            <cmd value="Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False" />
        </custom-item>
    </custom-items>
</config>


================================================
FILE: install.ps1
================================================
<#
 Copyright 2017 Google LLC

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
#>

<#
    .SYNOPSIS
        Installation script for FLARE VM.
        ** Only install on a virtual machine! **

    .DESCRIPTION
        Installation script for FLARE VM that leverages Chocolatey and Boxstarter.
        Script verifies minimal settings necessary to install FLARE VM on a virtual machine.
        Script allows users to customize package selection and envrionment variables used in FLARE VM via a GUI before installation begins.
        A CLI-only mode is also available by providing specific command-line arugment switches.

        To execute this script:
          1) Open PowerShell window as administrator
          2) Allow script execution by running command "Set-ExecutionPolicy Unrestricted"
          3) Unblock the install script by running "Unblock-File .\install.ps1"
          4) Execute the script by running ".\install.ps1"

    .PARAMETER password
        Current user password to allow reboot resiliency via Boxstarter. The script prompts for the password if not provided.

    .PARAMETER noPassword
        Switch parameter indicating a password is not needed for reboots.

    .PARAMETER customConfig
        Path to a configuration XML file. May be a file path or URL.

    .PARAMETER customLayout
        Path to a taskbar layout XML file. May be a file path or URL.

    .PARAMETER noWait
        Switch parameter to skip installation message before installation begins.

    .PARAMETER noGui
        Switch parameter to skip customization GUI.

    .PARAMETER noReboots
        Switch parameter to prevent reboots (not recommended).

    .PARAMETER noChecks
        Switch parameter to skip validation checks (not recommended).

    .EXAMPLE
        .\install.ps1

        Description
        ---------------------------------------
        Execute the installer to configure FLARE VM.

    .EXAMPLE
        .\install.ps1 -password Passw0rd! -noWait -noGui -noChecks

        Description
        ---------------------------------------
        CLI-only installation with minimal user interaction (some packages may require user interaction).
        To prevent reboots, also add the "-noReboots" switch.

    .EXAMPLE
        .\install.ps1 -customConfig "https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml"

        Description
        ---------------------------------------
        Use a custom configuration XML file hosted on the internet.

    .LINK
        https://github.com/mandiant/flare-vm
        https://github.com/mandiant/VM-Packages
#>

param (
  [string]$password = $null,
  [switch]$noPassword,
  [string]$customConfig = $null,
  [string]$customLayout = $null,
  [switch]$noWait,
  [switch]$noGui,
  [switch]$noReboots,
  [switch]$noChecks
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

# Function to download files and handle errors consistently
function Save-FileFromUrl {
    param (
        [string]$fileSource,
        [string]$fileDestination,
        [switch]$exitOnError
    )
    Write-Host "[+] Downloading file from '$fileSource'"
    try {
        (New-Object net.webclient).DownloadFile($fileSource,$FileDestination)
    } catch {
        Write-Host "`t[!] Failed to download '$fileSource'"
        Write-Host "`t[!] $_"
        if ($exitOnError) {
            Start-Sleep 3
            exit 1
        }
    }
}

# Function used for getting configuration files (such as config.xml and LayoutModification.xml)
function Get-ConfigFile {
    param (
        [string]$fileDestination,
        [string]$fileSource
    )
    # Check if the source is an existing file path.
    if (-not (Test-Path $fileSource)) {
        # If the source doesn't exist, assume it's a URL and download the file.
        Save-FileFromUrl -fileSource $fileSource -fileDestination $fileDestination
    } else {
        # If the source exists as a file, move it to the destination.
        Write-Host "[+] Using existing file as configuration file."
        Move-Item -Path $fileSource -Destination $fileDestination -Force
    }
}

# Set path to user's desktop
$desktopPath = [Environment]::GetFolderPath("Desktop")
Set-Location -Path $desktopPath -PassThru | Out-Null

# Setting global variables
$script:checksPassed = $true
$mandatoryChecksPassed = $true
$exit_message = "Installation cannot continue."

################################# Functions that conduct Pre-Install Checks #################################
# Function to test the network stack. Ping/GET requests to the resource to ensure that network stack looks good for installation
function Test-WebConnection {
    param (
        [string]$url
    )

    Write-Host "[+] Checking for Internet connectivity ($url)... (mandatory)"

    if (-not (Test-Connection $url -Quiet)) {
        return "It looks like you cannot ping $url. Check your network settings."
    }

    $response = $null
    try {
        $response = Invoke-WebRequest -Uri "https://$url" -UseBasicParsing -DisableKeepAlive
    }
    catch {
        return "Error accessing $url. Exception: $($_.Exception.Message)`n`t[!] Check your network settings."
    }

    if ($response -and $response.StatusCode -ne 200) {
        return "Unable to access $url. Status code: $($response.StatusCode)`n`t[!] Check your network settings."
    }

}


function Test-PSVersion{
    try {
		$psVersion = $PSVersionTable.PSVersion
		if ($psVersion -lt [System.Version]"5.0.0") {
		  return "Your PowerShell version ($psVersion) is not supported"
		}
	} catch {
		return "Unable to determine Powershell version"
	}
}

function Test-Admin {
    try {
		$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
		if (-not ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))){
			return "The script is not running as Administrator"
		}
	} catch {
		return "Unable to determine if the script is running as Administrator"
	}
}
function Test-ExecutionPolicy {
	try {
		if (-not((Get-ExecutionPolicy).ToString() -eq "Unrestricted")){
			return "You need to enable script execution with 'Set-ExecutionPolicy Unrestricted -Force'"
		}
	} catch {
		return "Unable to determine Powershell execution policy"
	}
}
function Test-DefenderAndTamperProtection {
        try {
		$defender = Get-Service -Name WinDefend -ea 0
		if ($null -ne $defender) {
			if ($defender.Status -eq "Running") {
				 return "Disable Windows Defender through Group Policy, reboot, and rerun installer"
			}
		}
        $tpEnabled = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows Defender\Features" -Name "TamperProtection" -ErrorAction Stop
        if ($tpEnabled.TamperProtection -eq 5) {
            return "Disable Tamper Protection, reboot, and rerun installer"
        }
    } catch {
		return "Unable to determine if TamperProtection and Defender are enabled"
    }
}

function Test-WindowsVersion {
	try {
		$os = Get-CimInstance -Class Win32_OperatingSystem
		$osMajorVersion = $os.Version.Split('.')[0] # Version examples: "6.1.7601", "10.0.19045"
		if ($osMajorVersion -lt 10) {
			return "Only Windows >= 10 is supported"
		}
	} catch {
		return "Unable to determine Windows Version"
	}
}

# 19045: https://www.microsoft.com/en-us/software-download/windows10ISO downloaded on April 25 2023.
# 20348: the version used by windows-2022 in GH actions
# 26100: https://www.microsoft.com/en-us/software-download/windows11 downloaded on May 6 2025.
function Test-TestedOS {
	$testedVersions = @(19045, 20348, 26100)
	try {
		$osVersion = (Get-CimInstance -class Win32_OperatingSystem).BuildNumber
		if (-not ($osVersion -in $testedVersions)){
			return "Windows version $osVersion has not been tested. Tested versions: $($testedVersions -join ', ')"
		}
	} catch {
		return "Windows version may not have been tested. Tested versions: $($testedVersions -join ', ')"
	}
}
function Test-VM {
    $virtualModels = @('VirtualBox', 'VMware', 'Virtual Machine', 'Hyper-V')
    try {
		$computerSystemModel = (Get-CimInstance win32_computersystem).model
		$isVirtualModel = $false

		foreach ($model in $virtualModels) {
			if ($computerSystemModel.Contains($model)) {
				$isVirtualModel = $true
				break
			}
		}

		if (-not ($isVirtualModel)) {
			return "You are not on a VM or have hardened your machine to not appear as such"
		}
	} catch {
		return "Unable to determine if you are on a VM"
	}
}

function Test-SpaceUserName {
	try {
		if (${Env:UserName} -match '\s') {
			return "Username '${Env:UserName}' contains a space and will break installation"
		}
	} catch {
		return "Unable to determine if the username contains a space"
	}
}
function Test-Storage {
	try {
		$disk = Get-PSDrive (Get-Location).Drive.Name
		Start-Sleep -Seconds 1
		if (-not (($disk.used + $disk.free)/1GB -gt 58.8)) {
			return "A minimum of 60 GB hard drive space is preferred, increase hard drive space"
		}
	} catch {
		return "Unable to determine hard drive space"
	}
}



if ($noGui.IsPresent) {
	if (-not $noChecks.IsPresent) {
		# Check PowerShell version
		Write-Host "[+] Checking if PowerShell version is compatible (mandatory)..."
		$error_info = Test-PSVersion
		if ($error_info){
			Write-Host "`t[!] $error_info" -ForegroundColor Red
			$mandatoryChecksPassed = $false
		}

		# Ensure script is ran as administrator
		Write-Host "[+] Checking if script is running as administrator (mandatory)..."
		$error_info = Test-Admin
		if ($error_info) {
			Write-Host "`t[!] $error_info"  -ForegroundColor Red
			$mandatoryChecksPassed = $false
		}

		# Ensure execution policy is unrestricted
		Write-Host "[+] Checking if execution policy is unrestricted.. (mandatory)."
		$error_info = Test-ExecutionPolicy
		if ($error_info) {
			Write-Host "`t[!] $error_info" -ForegroundColor Red
			$mandatoryChecksPassed = $false
		}

		# Check if Windows < 10
		Write-Host "[+] Checking Operating System version compatibility..."
		$error_info = Test-WindowsVersion
		if ($error_info) {
			Write-Host "`t[!] $error_info" -ForegroundColor Yellow
			$script:checksPassed = $false
		}

		# Check if host has been tested
		Write-Host "[+] Checking if the Operating System has been tested..."
		$error_info= Test-TestedOS
		if ($error_info) {
			Write-Host "`t[!] $error_info" -ForegroundColor Yellow
			$script:checksPassed = $false
		}

		# Check if system is a virtual machine
		Write-Host "[+] Checking if the system runs on a Virtual Machine..."
		$error_info = Test-VM
		if ($error_info) {
			Write-Host "`t[!] $error_info" -ForegroundColor Yellow
			$script:checksPassed = $false
		}

		# Check for spaces in the username, exit if identified
		Write-Host "[+] Checking for spaces in the username... (mandatory)"
		$error_info = Test-SpaceUserName
		if ($error_info) {
			Write-Host "`t[!] $error_info" -ForegroundColor Red
			$mandatoryChecksPassed = $false
		}

		# Check if host has enough disk space
		Write-Host "[+] Checking if host has enough disk space..."
		$error_info = Test-Storage
		if ($error_info) {
			Write-Host "`t[!] $error_info"   -ForegroundColor Yellow
			$script:checksPassed = $false
		}

		# Internet connectivity checks
		$error_info = Test-WebConnection 'google.com'
		if ($error_info){
			Write-Host "`t[+] $error_info" -ForegroundColor Red
			$mandatoryChecksPassed = $false
		}else {
			$error_info = Test-WebConnection 'github.com'
			if ($error_info){
				Write-Host "`t[+] $error_info" -ForegroundColor Red
			    $mandatoryChecksPassed = $false
			}else {
				$error_info = Test-WebConnection 'raw.githubusercontent.com'
				if ($error_info){
				    Write-Host "`t[+] $error_info" -ForegroundColor Red
			        $mandatoryChecksPassed = $false
			    }
			}
		}

		# Check if Tamper Protection is disabled
		Write-Host "[+] Checking if Windows Defender Tamper Protection is disabled..."
		$error_info = Test-DefenderAndTamperProtection
		if ($error_info) {
			Write-Host "`t[!]$errorinfo"  -ForegroundColor Red
			$script:checksPassed = $false
		}

		if (-not $mandatoryChecksPassed){
			Write-Host "[!] $exit_message" -ForegroundColor Red
			Start-Sleep 3
            exit 1
		}

		if (-not $script:checksPassed){
			Write-Host "[-] Do you still wish to proceed? (Y/N): " -ForegroundColor Yellow -NoNewline
			$response = Read-Host
			if ($response -notin @("y","Y")) {
				exit 1
			}
		}

		Write-Host "[+] Setting password to never expire to avoid that a password expiration blocks the installation..."
		$UserNoPasswd = Get-CimInstance Win32_UserAccount -Filter "Name='${Env:UserName}'"
		$UserNoPasswd | Set-CimInstance -Property @{ PasswordExpires = $false }

		# Prompt user to remind them to take a snapshot
		Write-Host "[-] Have you taken a VM snapshot to ensure you can revert to pre-installation state? (Y/N): " -ForegroundColor Yellow -NoNewline
		$response = Read-Host
		if ($response -notin @("y","Y")) {
			exit 1
		}
	}

}

function Open-CheckManager {
	if ($formChecksManager.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) {
		exit
	}
}
# Init Window Install checks
if (-not $noGui.IsPresent) {

    Write-Host "[+] Starting GUI to allow user to edit configuration file..."
    ################################################################################
    ## BEGIN GUI
    ################################################################################
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -Assembly System.Drawing

    $errorColor = [System.Drawing.ColorTranslator]::FromHtml("#c80505")
    $successColor = [System.Drawing.ColorTranslator]::FromHtml("#417505")
    $grayedColor = [System.Drawing.ColorTranslator]::FromHtml("#6e6964")
	$orangeColor = [System.Drawing.ColorTranslator]::FromHtml("#bf8334")

    if (-not $noChecks.IsPresent) {

		#################################################################################################
		################################ Installer Checks Form Controls #################################
		#################################################################################################

		$formChecksManager           = New-Object system.Windows.Forms.Form
		$formChecksManager.ClientSize  = New-Object System.Drawing.Point(700,640)
		$formChecksManager.text      = "FLAREVM Pre-Install Checks"
		$formChecksManager.TopMost   = $true
		$formChecksManager.StartPosition = 'CenterScreen'

		$ChecksPanel                     = New-Object system.Windows.Forms.Panel
		$ChecksPanel.height              = 460
		$ChecksPanel.width               = 89
		$ChecksPanel.location            = New-Object System.Drawing.Point(570,8)

		$InstallChecksGroup              = New-Object system.Windows.Forms.Groupbox
		$InstallChecksGroup.height       = 490
		$InstallChecksGroup.width        = 665
		$InstallChecksGroup.text         = "Installation Checks"
		$InstallChecksGroup.location     = New-Object System.Drawing.Point(23,14)

		################################# Check Labels #################################

		$PSVersionLabel = New-Object system.Windows.Forms.Label
		$PSVersionLabel.text = "Valid Powershell version"
		$PSVersionLabel.AutoSize = $true
		$PSVersionLabel.width = 25
		$PSVersionLabel.height = 10
		$PSVersionLabel.location = New-Object System.Drawing.Point(15,18)
		$PSVersionLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

        $RunningAsAdminLabel = New-Object system.Windows.Forms.Label
		$RunningAsAdminLabel.text = "Running as Administrator"
		$RunningAsAdminLabel.AutoSize = $true
		$RunningAsAdminLabel.width = 25
		$RunningAsAdminLabel.height = 10
		$RunningAsAdminLabel.location = New-Object System.Drawing.Point(15,59)
		$RunningAsAdminLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$ExecutionPolicyLabel = New-Object system.Windows.Forms.Label
		$ExecutionPolicyLabel.text = "Execution Policy Unrestricted"
		$ExecutionPolicyLabel.AutoSize = $true
		$ExecutionPolicyLabel.width = 25
		$ExecutionPolicyLabel.height = 10
		$ExecutionPolicyLabel.location = New-Object System.Drawing.Point(15,104)
		$ExecutionPolicyLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$validWindowsVersionLabel = New-Object system.Windows.Forms.Label
		$validWindowsVersionLabel.text = "Valid Windows Version"
		$validWindowsVersionLabel.AutoSize = $true
		$validWindowsVersionLabel.location = New-Object System.Drawing.Point(15,149)
		$validWindowsVersionLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$WindowsReleaseLabel = New-Object system.Windows.Forms.Label
		$WindowsReleaseLabel.text = "Tested Windows Version"
		$WindowsReleaseLabel.AutoSize = $true
		$WindowsReleaseLabel.width = 25
		$WindowsReleaseLabel.height = 10
		$WindowsReleaseLabel.location = New-Object System.Drawing.Point(15,193)
		$WindowsReleaseLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$RunningVMLabel = New-Object system.Windows.Forms.Label
		$RunningVMLabel.text = "Running in a Virtual Machine"
		$RunningVMLabel.AutoSize = $true
		$RunningVMLabel.width = 25
		$RunningVMLabel.height = 10
		$RunningVMLabel.location = New-Object System.Drawing.Point(15,239)
		$RunningVMLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$usernameContainsSpacesLabel = New-Object system.Windows.Forms.Label
		$usernameContainsSpacesLabel.text = "Valid username"
		$usernameContainsSpacesLabel.AutoSize = $true
		$usernameContainsSpacesLabel.location = New-Object System.Drawing.Point(15,285)
		$usernameContainsSpacesLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$EnoughHardStorageLabel = New-Object system.Windows.Forms.Label
		$EnoughHardStorageLabel.text = "Enough Hard Drive Space"
		$EnoughHardStorageLabel.AutoSize = $true
		$EnoughHardStorageLabel.width = 25
		$EnoughHardStorageLabel.height = 10
		$EnoughHardStorageLabel.location = New-Object System.Drawing.Point(15,325)
		$EnoughHardStorageLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$internetConnectivityLabel = New-Object system.Windows.Forms.Label
		$internetConnectivityLabel.text = "Internet connectivity"
		$internetConnectivityLabel.AutoSize = $true
		$internetConnectivityLabel.location = New-Object System.Drawing.Point(15,369)
		$internetConnectivityLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$WindowsDefenderLabel = New-Object system.Windows.Forms.Label
		$WindowsDefenderLabel.text = "Windows Defender Disabled"
		$WindowsDefenderLabel.AutoSize = $true
		$WindowsDefenderLabel.width = 25
		$WindowsDefenderLabel.height = 10
		$WindowsDefenderLabel.location = New-Object System.Drawing.Point(15,411)
		$WindowsDefenderLabel.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		################################# Check Boolean Controls #################################

		$PSVersion = New-Object system.Windows.Forms.Label
		$PSVersion.text = "False"
		$PSVersion.AutoSize = $true
		$PSVersion.width = 25
		$PSVersion.height = 10
		$PSVersion.location = New-Object System.Drawing.Point(24,18)
		$PSVersion.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$PSVersion.ForeColor = $errorColor

		$RunningAsAdmin = New-Object system.Windows.Forms.Label
		$RunningAsAdmin.text = "False"
		$RunningAsAdmin.AutoSize = $true
		$RunningAsAdmin.width = 25
		$RunningAsAdmin.height = 10
		$RunningAsAdmin.location = New-Object System.Drawing.Point(24,63)
		$RunningAsAdmin.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$RunningAsAdmin.ForeColor = $errorColor

		$ExecutionPolicy = New-Object system.Windows.Forms.Label
		$ExecutionPolicy.text = "False"
		$ExecutionPolicy.AutoSize = $true
		$ExecutionPolicy.width = 25
		$ExecutionPolicy.height = 10
		$ExecutionPolicy.location = New-Object System.Drawing.Point(24,108)
		$ExecutionPolicy.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$ExecutionPolicy.ForeColor = $errorColor

		$validWindowsVersion = New-Object system.Windows.Forms.Label
		$validWindowsVersion.text = "False"
		$validWindowsVersion.AutoSize = $true
		$validWindowsVersion.width = 25
		$validWindowsVersion.height = 10
		$validWindowsVersion.location = New-Object System.Drawing.Point(24,150)
		$validWindowsVersion.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$validWindowsVersion.ForeColor = $errorColor

		$WindowsRelease = New-Object system.Windows.Forms.Label
		$WindowsRelease.text = "False"
		$WindowsRelease.AutoSize = $true
		$WindowsRelease.width = 25
		$WindowsRelease.height = 10
		$WindowsRelease.location = New-Object System.Drawing.Point(24,195)
		$WindowsRelease.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$WindowsRelease.ForeColor = $orangeColor

		$RunningVM = New-Object system.Windows.Forms.Label
		$RunningVM.text = "False"
		$RunningVM.AutoSize = $true
		$RunningVM.width = 25
		$RunningVM.height = 10
		$RunningVM.location = New-Object System.Drawing.Point(24,240)
		$RunningVM.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$RunningVM.ForeColor = $orangeColor

		$usernameContainsSpaces = New-Object system.Windows.Forms.Label
		$usernameContainsSpaces.text = "False"
		$usernameContainsSpaces.AutoSize = $true
		$usernameContainsSpaces.width = 25
		$usernameContainsSpaces.height = 10
		$usernameContainsSpaces.location = New-Object System.Drawing.Point(24,285)
		$usernameContainsSpaces.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$usernameContainsSpaces.ForeColor = $errorColor

		$EnoughHardStorage = New-Object system.Windows.Forms.Label
		$EnoughHardStorage.text = "False"
		$EnoughHardStorage.AutoSize = $true
		$EnoughHardStorage.width = 25
		$EnoughHardStorage.height = 10
		$EnoughHardStorage.location = New-Object System.Drawing.Point(24,322)
		$EnoughHardStorage.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$EnoughHardStorage.ForeColor = $orangeColor

		$internetConnectivity = New-Object system.Windows.Forms.Label
		$internetConnectivity.text = "False"
		$internetConnectivity.AutoSize = $true
		$internetConnectivity.width = 25
		$internetConnectivity.height = 10
		$internetConnectivity.location = New-Object System.Drawing.Point(24,368)
		$internetConnectivity.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$internetConnectivity.ForeColor = $errorColor

		$WindowsDefender = New-Object system.Windows.Forms.Label
		$WindowsDefender.text = "False"
		$WindowsDefender.AutoSize = $true
		$WindowsDefender.width = 25
		$WindowsDefender.height = 10
		$WindowsDefender.location = New-Object System.Drawing.Point(24,409)
		$WindowsDefender.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
		$WindowsDefender.ForeColor = $orangeColor

		################################# Check Tooltip Controls #################################
		$verticalPosition = 41

		# $PSVersionTooltip
		$PSVersionTooltip = New-Object system.Windows.Forms.Label
		$PSVersionTooltip.text = "Powershell version must be >= 5 (mandatory)"
		$PSVersionTooltip.AutoSize = $true
		$PSVersionTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$PSVersionTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$PSVersionTooltip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $RunningAsAdminTooltip
		$RunningAsAdminTooltip = New-Object system.Windows.Forms.Label
		$RunningAsAdminTooltip.text = "You must run the script as Administrator (mandatory)"
		$RunningAsAdminTooltip.AutoSize = $true
		$RunningAsAdminTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$RunningAsAdminTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$RunningAsAdminTooltip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $ExecutionPolicyTooltip
		$ExecutionPolicyTooltip = New-Object system.Windows.Forms.Label
		$ExecutionPolicyTooltip.text = "You must enable script execution (mandatory)"
		$ExecutionPolicyTooltip.AutoSize = $true
		$ExecutionPolicyTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$ExecutionPolicyTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$ExecutionPolicyTooltip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $validWindowsVersionToolTip
		$validWindowsVersionToolTip = New-Object system.Windows.Forms.Label
		$validWindowsVersionToolTip.text = "Only Windows Version >= 10 is supported (mandatory)"
		$validWindowsVersionToolTip.AutoSize = $true
		$validWindowsVersionToolTip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$validWindowsVersionToolTip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$validWindowsVersionToolTip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $WindowsReleaseTooltip
		$WindowsReleaseTooltip = New-Object system.Windows.Forms.Label
		$WindowsReleaseTooltip.text = "You might run into issues when using a non tested version"
		$WindowsReleaseTooltip.AutoSize = $true
		$WindowsReleaseTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$WindowsReleaseTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$WindowsReleaseTooltip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $RunningVMTooltip
		$RunningVMTooltip = New-Object system.Windows.Forms.Label
		$RunningVMTooltip.text = "Only run this script inside a Virtual Machine (VM)"
		$RunningVMTooltip.AutoSize = $true
		$RunningVMTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$RunningVMTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$RunningVMTooltip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $usernameContainsSpacesToolTip
		$usernameContainsSpacesToolTip = New-Object system.Windows.Forms.Label
		$usernameContainsSpacesToolTip.text = "Username cannot contain spaces (mandatory)"
		$usernameContainsSpacesToolTip.AutoSize = $true
		$usernameContainsSpacesToolTip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$usernameContainsSpacesToolTip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$usernameContainsSpacesToolTip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $EnoughHardStorageTooltip
		$EnoughHardStorageTooltip = New-Object system.Windows.Forms.Label
		$EnoughHardStorageTooltip.text = "A minimum of 60 GB hard drive space is preferred"
		$EnoughHardStorageTooltip.AutoSize = $true
		$EnoughHardStorageTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$EnoughHardStorageTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$EnoughHardStorageTooltip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $internetConnectivityTooltip
		$internetConnectivityTooltip = New-Object system.Windows.Forms.Label
		$internetConnectivityTooltip.text = "You must have internet connection (mandatory)"
		$internetConnectivityTooltip.AutoSize = $true
		$internetConnectivityTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$internetConnectivityTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$internetConnectivityTooltip.ForeColor = $grayedColor
		$verticalPosition += 44

		# $WindowsDefenderTooltip
		$WindowsDefenderTooltip = New-Object system.Windows.Forms.Label
		$WindowsDefenderTooltip.text = "Disable Windows Defender and Tamper Protection"
		$WindowsDefenderTooltip.AutoSize = $true
		$WindowsDefenderTooltip.location = New-Object System.Drawing.Point(15,$verticalPosition)
		$WindowsDefenderTooltip.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$WindowsDefenderTooltip.ForeColor = $grayedColor


		################################# Check Completion Controls #################################

		$breakInstallationLabel                = New-Object system.Windows.Forms.Label
		$breakInstallationLabel.Text           = $exit_message
		$breakInstallationLabel.AutoSize       = $true
		$breakInstallationLabel.location       = New-Object System.Drawing.Point(40,530)
		$breakInstallationLabel.Font           = New-Object System.Drawing.Font('Microsoft Sans Serif',12)
		$breakInstallationLabel.ForeColor      = $errorColor
		$breakInstallationLabel.Visible        = $false

		$BreakMyInstallCheckbox          = New-Object system.Windows.Forms.CheckBox
		$BreakMyInstallCheckbox.Visible  = $false
		$BreakMyInstallCheckbox.text     = "I understand that continuing without satisfying all pre-install checks might cause install issues"
		$BreakMyInstallCheckbox.AutoSize = $true
		$BreakMyInstallCheckbox.width    = 324
		$BreakMyInstallCheckbox.height   = 21
		$BreakMyInstallCheckbox.location = New-Object System.Drawing.Point(30,510)
		$BreakMyInstallCheckbox.Font     = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

        $snapshotCheckBox 	             = New-Object system.Windows.Forms.CheckBox
		$snapshotCheckBox.Text           = "I have taken a VM snapshot to ensure I can revert to pre-installation state"
		$snapshotCheckBox.AutoSize       = $true
		$snapshotCheckBox.location       = New-Object System.Drawing.Point(30,532)
		$snapshotCheckBox.Font           = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$snapshotCheckBox.Visible 	     = $false

		$ChecksCompleteButton            = New-Object system.Windows.Forms.Button
		$ChecksCompleteButton.text       = "Continue"
		$ChecksCompleteButton.width      = 97
		$ChecksCompleteButton.height     = 37
		$ChecksCompleteButton.enabled    = $false
		$ChecksCompleteButton.DialogResult   = [System.Windows.Forms.DialogResult]::OK
		$ChecksCompleteButton.location   = New-Object System.Drawing.Point(420,565)
		$ChecksCompleteButton.Font       = New-Object System.Drawing.Font('Microsoft Sans Serif',12)
		$ChecksCompleteButton.Add_Click({
			$script:checksPassed = $true
			[void]$formChecksManager.Close()
		})

		$checksCancelButton            = New-Object system.Windows.Forms.Button
		$checksCancelButton.Text       = "Cancel"
		$checksCancelButton.width      = 97
		$checksCancelButton.height     = 37
		$checksCancelButton.location   = New-Object System.Drawing.Point(519,565)
		$checksCancelButton.Font       = New-Object System.Drawing.Font('Microsoft Sans Serif',12)
		$checksCancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel

		$InstallChecksGroup.controls.AddRange(@($ChecksPanel,$RunningAsAdminLabel,$ExecutionPolicyLabel,$WindowsDefenderLabel,$WindowsReleaseLabel,$RunningVMLabel,$PSVersionLabel,$internetConnectivityLabel,$validWindowsVersionLabel,$validWindowsVersionToolTip,$RunningAsAdminTooltip,$ExecutionPolicyTooltip,$WindowsDefenderTooltip,$WindowsReleaseTooltip,$RunningVMTooltip,$EnoughHardStorageLabel, $EnoughHardStorageTooltip,$PSVersionTooltip,$internetConnectivityTooltip,$usernameContainsSpacesLabel,$usernameContainsSpacesToolTip,$RunningAsAdmin,$EnoughHardStorage))
		$formChecksManager.controls.AddRange(@($InstallChecksGroup,$ChecksCompleteButton,$checksCancelButton,$BreakMyInstallCheckbox,$snapshotCheckBox,$breakInstallationLabel))
		$ChecksPanel.controls.AddRange(@($RunningAsAdmin, $ExecutionPolicy,$WindowsDefender,$WindowsRelease,$RunningVM, $EnoughHardStorage, $PSVersion, $internetConnectivity, $validWindowsVersion,$usernameContainsSpaces ))

	    # Make sure that the user completed all pre-install steps
		$error_info = Test-Admin
		if ($error_info){
			$RunningAsAdmin.Text = $error_info
			$RunningAsAdmin.Forecolor = $errorColor
            $mandatoryChecksPassed = $false
        } else {
			$RunningAsAdmin.Text = "True"
            $RunningAsAdmin.ForeColor = $successColor
        }
		$error_info = Test-ExecutionPolicy
		if ($error_info){
			$ExecutionPolicyTooltip.Text = $error_info
			$ExecutionPolicyTooltip.Forecolor = $errorColor
            $mandatoryChecksPassed = $false
        } else {
			$ExecutionPolicy.Text = "True"
            $ExecutionPolicy.ForeColor = $successColor
        }
		$error_info = Test-DefenderAndTamperProtection
		if ($error_info){
			$WindowsDefenderTooltip.Text = $error_info
			$WindowsDefenderTooltip.Forecolor = $orangeColor
            $script:checksPassed = $false
        } else {
			$WindowsDefender.Text = "True"
            $WindowsDefender.ForeColor = $successColor
        }

		$error_info = Test-TestedOS
		if ($error_info){
            $WindowsReleaseTooltip.Text = $error_info
			$WindowsReleaseTooltip.Forecolor = $orangeColor
            $script:checksPassed = $false
        } else {
			$WindowsRelease.Text = "True"
            $WindowsRelease.ForeColor = $successColor
        }
		$error_info = Test-VM
		if ($error_info){
            $RunningAsAdminTooltip.Text = $error_info
			$RunningAsAdminTooltip.Forecolor = $orangeColor
            $script:checksPassed = $false
        } else {
			$RunningVM.Text = "True"
            $RunningVM.ForeColor = $successColor
        }
		$error_info = Test-Storage
		if ($error_info){
            $EnoughHardStorageTooltip.Forecolor = $orangeColor
			$EnoughHardStorageTooltip.Text = $error_info
            $script:checksPassed = $false
        } else {
			$EnoughHardStorage.Text = "True"
            $EnoughHardStorage.ForeColor = $successColor
        }
		$error_info = Test-PSVersion
		if ($error_info){
			$PSVersionTooltip.Text = $error_info
			$PSVersionTooltip.Forecolor = $errorColor
            $MandatoryChecksPassed = $false
		} else {
			$PSVersion.Text = "True"
			$PSVersion.ForeColor = $successColor
        }

		$error_info = Test-WebConnection 'google.com'
		if ($error_info){
			$internetConnectivityTooltip.Text = $error_info
			$internetConnectivityTooltip.Forecolor = $errorColor
			$mandatoryChecksPassed = $false
		}else {
			$error_info = Test-WebConnection 'github.com'
			if ($error_info){
				$internetConnectivityTooltip.Text = $error_info
				$internetConnectivityTooltip.Forecolor = $errorColor
			    $mandatoryChecksPassed = $false
			}else {
				$error_info = Test-WebConnection 'raw.githubusercontent.com'
				if ($error_info){
				    $internetConnectivityTooltip.Text = $error_info
					$internetConnectivityTooltip.Forecolor = $errorColor
			        $mandatoryChecksPassed = $false
			    } else {
	                $internetConnectivity.Text = "True"
			        $internetConnectivity.ForeColor = $successColor
				}
			}
		}
		$error_info = Test-WindowsVersion
		if ($error_info){
			$validWindowsVersionToolTip.Text = $error_info
			$validWindowsVersionToolTip.Forecolor = $errorColor
			$mandatoryChecksPassed = $false
		} else {
			$validWindowsVersion.Text = "True"
			$validWindowsVersion.ForeColor = $successColor
		}
		$error_info = Test-SpaceUserName
		if ($error_info){
			$usernameContainsSpacesToolTip.Text = $error_info
			$usernameContainsSpacesToolTip.Forecolor = $errorColor
			$mandatoryChecksPassed = $false
		}else {
			$usernameContainsSpaces.Text = "True"
			$usernameContainsSpaces.ForeColor = $successColor
		}

		#only display the checkbox if some checks did not pass
		if ($mandatoryChecksPassed){
			if ($script:checksPassed){
			    $BreakMyInstallCheckbox.Visible = $false
			    $snapshotCheckBox.Visible = $true
			}else{
				$BreakMyInstallCheckbox.Visible = $true
				$snapshotCheckBox.Visible = $true
			}
		}else{
			$breakInstallationLabel.visible = $true
		}

		$snapshotCheckBox.Add_CheckStateChanged({
			if (($snapshotCheckBox.Checked) -and ($script:checksPassed)){
				$ChecksCompleteButton.enabled = $true
			} else {
				if (($snapshotCheckBox.Checked) -and (-not $script:checksPassed)){
				   $ChecksCompleteButton.enabled = $BreakMyInstallCheckbox.Checked
			    } else{
				    if (-not ($snapshotCheckBox.Checked)){
				        $ChecksCompleteButton.enabled = $false
					}
			    }
			}
		})

		$BreakMyInstallCheckbox.Add_CheckStateChanged({
			if ($BreakMyInstallCheckbox.Checked){
				  $ChecksCompleteButton.enabled = $snapshotCheckBox.Checked
			} else{
			   $ChecksCompleteButton.enabled = $false
			}
		})
        Open-CheckManager
	}
    # init GUI controls of the install customization Window
    $formEnv                   = New-Object system.Windows.Forms.Form
    $formEnv.ClientSize        = New-Object System.Drawing.Point(750,350)
    $formEnv.text              = "FLARE VM Install Customization"
    $formEnv.TopMost           = $true
    $formEnv.MaximizeBox       = $false
    $formEnv.FormBorderStyle   = 'FixedDialog'
    $formEnv.StartPosition     = 'CenterScreen'

    $envVarGroup            = New-Object system.Windows.Forms.Groupbox
    $envVarGroup.height     = 201
    $envVarGroup.width      = 690
    $envVarGroup.text       = "Environment Variable Customization"
    $envVarGroup.location   = New-Object System.Drawing.Point(15,59)

    $welcomeLabel           = New-Object system.Windows.Forms.Label
    $welcomeLabel.text      = "Welcome to FLARE VM's custom installer. Please select your options below.`nDefault values will be used if you make no modifications."
    $welcomeLabel.AutoSize  = $true
    $welcomeLabel.width     = 25
    $welcomeLabel.height    = 10
    $welcomeLabel.location  = New-Object System.Drawing.Point(15,14)
    $welcomeLabel.Font      = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $vmCommonDirText                 = New-Object system.Windows.Forms.TextBox
    $vmCommonDirText.multiline       = $false
    $vmCommonDirText.width           = 385
    $vmCommonDirText.height          = 20
    $vmCommonDirText.location        = New-Object System.Drawing.Point(190,21)
    $vmCommonDirText.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $vmCommonDirSelect               = New-Object system.Windows.Forms.Button
    $vmCommonDirSelect.text          = "Select Folder"
    $vmCommonDirSelect.width         = 95
    $vmCommonDirSelect.height        = 30
    $vmCommonDirSelect.location      = New-Object System.Drawing.Point(588,17)
    $vmCommonDirSelect.Font          = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
    $selectFolderArgs1 = @{textBox=$vmCommonDirText; envVar="VM_COMMON_DIR"}
    $vmCommonDirSelect.Add_Click({Get-Folder @selectFolderArgs1})

    $vmCommonDirLabel                = New-Object system.Windows.Forms.Label
    $vmCommonDirLabel.text           = "%VM_COMMON_DIR%"
    $vmCommonDirLabel.AutoSize       = $true
    $vmCommonDirLabel.width          = 25
    $vmCommonDirLabel.height         = 10
    $vmCommonDirLabel.location       = New-Object System.Drawing.Point(2,24)
    $vmCommonDirLabel.Font           = New-Object System.Drawing.Font('Microsoft Sans Serif',9.5,[System.Drawing.FontStyle]::Bold)

    $vmCommonDirNote                 = New-Object system.Windows.Forms.Label
    $vmCommonDirNote.text            = "Shared module and metadata for VM (e.g., config, logs, etc...)"
    $vmCommonDirNote.AutoSize        = $true
    $vmCommonDirNote.width           = 25
    $vmCommonDirNote.height          = 10
    $vmCommonDirNote.location        = New-Object System.Drawing.Point(190,46)
    $vmCommonDirNote.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $toolListDirText                 = New-Object system.Windows.Forms.TextBox
    $toolListDirText.multiline       = $false
    $toolListDirText.width           = 385
    $toolListDirText.height          = 20
    $toolListDirText.location        = New-Object System.Drawing.Point(190,68)
    $toolListDirText.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $toolListDirSelect               = New-Object system.Windows.Forms.Button
    $toolListDirSelect.text          = "Select Folder"
    $toolListDirSelect.width         = 95
    $toolListDirSelect.height        = 30
    $toolListDirSelect.location      = New-Object System.Drawing.Point(588,64)
    $toolListDirSelect.Font          = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
    $selectFolderArgs2 = @{textBox=$toolListDirText; envVar="TOOL_LIST_DIR"}
    $toolListDirSelect.Add_Click({Get-Folder @selectFolderArgs2})

    $toolListDirLabel                = New-Object system.Windows.Forms.Label
    $toolListDirLabel.text           = "%TOOL_LIST_DIR%"
    $toolListDirLabel.AutoSize       = $true
    $toolListDirLabel.width          = 25
    $toolListDirLabel.height         = 10
    $toolListDirLabel.location       = New-Object System.Drawing.Point(2,71)
    $toolListDirLabel.Font           = New-Object System.Drawing.Font('Microsoft Sans Serif',9.5,[System.Drawing.FontStyle]::Bold)

    $toolListDirNote                 = New-Object system.Windows.Forms.Label
    $toolListDirNote.text            = "Folder to store tool categories and shortcuts"
    $toolListDirNote.AutoSize        = $true
    $toolListDirNote.width           = 25
    $toolListDirNote.height          = 10
    $toolListDirNote.location        = New-Object System.Drawing.Point(190,94)
    $toolListDirNote.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $rawToolsDirText                 = New-Object system.Windows.Forms.TextBox
    $rawToolsDirText.multiline       = $false
    $rawToolsDirText.width           = 385
    $rawToolsDirText.height          = 20
    $rawToolsDirText.location        = New-Object System.Drawing.Point(190,113)
    $rawToolsDirText.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $rawToolsDirSelect               = New-Object system.Windows.Forms.Button
    $rawToolsDirSelect.text          = "Select Folder"
    $rawToolsDirSelect.width         = 95
    $rawToolsDirSelect.height        = 30
    $rawToolsDirSelect.location      = New-Object System.Drawing.Point(588,109)
    $rawToolsDirSelect.Font          = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
    $selectFolderArgs4 = @{textBox=$rawToolsDirText; envVar="RAW_TOOLS_DIR"}
    $rawToolsDirSelect.Add_Click({Get-Folder @selectFolderArgs4})

    $rawToolsDirLabel                = New-Object system.Windows.Forms.Label
    $rawToolsDirLabel.text           = "%RAW_TOOLS_DIR%"
    $rawToolsDirLabel.AutoSize       = $true
    $rawToolsDirLabel.width          = 25
    $rawToolsDirLabel.height         = 10
    $rawToolsDirLabel.location       = New-Object System.Drawing.Point(2,116)
    $rawToolsDirLabel.Font           = New-Object System.Drawing.Font('Microsoft Sans Serif',9.5,[System.Drawing.FontStyle]::Bold)

    $rawToolsDirNote                 = New-Object system.Windows.Forms.Label
    $rawToolsDirNote.text            = "Folder to store downloaded tools"
    $rawToolsDirNote.AutoSize        = $true
    $rawToolsDirNote.width           = 25
    $rawToolsDirNote.height          = 10
    $rawToolsDirNote.location        = New-Object System.Drawing.Point(190,137)
    $rawToolsDirNote.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $okButton                        = New-Object system.Windows.Forms.Button
    $okButton.text                   = "Continue"
    $okButton.width                  = 97
    $okButton.height                 = 37
    $okButton.location               = New-Object System.Drawing.Point(480,280)
    $okButton.Font                   = New-Object System.Drawing.Font('Microsoft Sans Serif',11)
    $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK

    $cancelButton                    = New-Object system.Windows.Forms.Button
    $cancelButton.text               = "Cancel"
    $cancelButton.width              = 97
    $cancelButton.height             = 37
    $cancelButton.location           = New-Object System.Drawing.Point(580,280)
    $cancelButton.Font               = New-Object System.Drawing.Font('Microsoft Sans Serif',11)
    $cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel

    $formEnv.controls.AddRange(@($envVarGroup,$okButton,$cancelButton,$welcomeLabel))
    $formEnv.AcceptButton = $okButton
    $formEnv.CancelButton = $cancelButton

    $envVarGroup.controls.AddRange(@($vmCommonDirText,$vmCommonDirSelect,$vmCommonDirLabel,$toolListDirText,$toolListDirSelect,$toolListDirLabel,$toolListShortCutText,$toolListShortcutSelect,$toolListShortcutLabel,$vmCommonDirNote,$toolListDirNote,$toolListShortcutNote,$rawToolsDirText,$rawToolsDirSelect,$rawToolsDirLabel,$rawToolsDirNote))

}
if (-not $noPassword.IsPresent) {
    # Get user credentials for autologin during reboots
    if ([string]::IsNullOrEmpty($password)) {
        Write-Host "[+] Getting user credentials ..."
        Set-ItemProperty "HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds" -Name "ConsolePrompting" -Value $True
        Start-Sleep -Milliseconds 500
        $credentials = Get-Credential ${Env:UserName}
    } else {
        $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
        $credentials = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList ${Env:UserName}, $securePassword
    }
}

# Check Boxstarter version
$boxstarterVersionGood = $false
if (${Env:ChocolateyInstall} -and (Test-Path "${Env:ChocolateyInstall}\bin\choco.exe")) {
    choco info -l -r "boxstarter" | ForEach-Object { $name, $version = $_ -split '\|' }
    $boxstarterVersionGood = [System.Version]$version -ge [System.Version]"3.0.2"
}

# Install Boxstarter if needed
if (-not $boxstarterVersionGood) {
    Write-Host "[+] Installing Boxstarter..." -ForegroundColor Cyan
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
    Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://boxstarter.org/bootstrapper.ps1'))
    Get-Boxstarter -Force

    Start-Sleep -Milliseconds 500
}
Import-Module "${Env:ProgramData}\boxstarter\boxstarter.chocolatey\boxstarter.chocolatey.psd1" -Force

# Check Chocolatey version
$version = choco --version
$chocolateyVersionGood = [System.Version]$version -ge [System.Version]"2.0.0"

# Update Chocolatey if needed
if (-not ($chocolateyVersionGood)) { choco upgrade chocolatey }

# Attempt to disable updates (i.e., windows updates and store updates)
Write-Host "[+] Attempting to disable updates..."
Disable-MicrosoftUpdate
try {
  New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\WindowsStore" -Name "AutoDownload" -PropertyType DWord -Value 2 -ErrorAction Stop -Force | Out-Null
} catch {
  Write-Host "`t[!] Failed to disable Microsoft Store updates" -ForegroundColor Yellow
}

# Set Boxstarter options
$Boxstarter.RebootOk = (-not $noReboots.IsPresent)
$Boxstarter.NoPassword = $noPassword.IsPresent
$Boxstarter.AutoLogin = $true
$Boxstarter.SuppressLogging = $True
$VerbosePreference = "SilentlyContinue"
Set-BoxstarterConfig -NugetSources "$desktopPath;.;https://www.myget.org/F/vm-packages/api/v2;https://myget.org/F/vm-packages/api/v2;https://chocolatey.org/api/v2"
Set-WindowsExplorerOptions -EnableShowHiddenFilesFoldersDrives -EnableShowProtectedOSFiles -EnableShowFileExtensions -EnableShowFullPathInTitleBar

# Set Chocolatey options
Write-Host "[+] Updating Chocolatey settings..."
choco sources add -n="vm-packages" -s "$desktopPath;.;https://www.myget.org/F/vm-packages/api/v2;https://myget.org/F/vm-packages/api/v2" --priority 1
choco feature enable -n allowGlobalConfirmation
choco feature enable -n allowEmptyChecksums
$cache = "${Env:LocalAppData}\ChocoCache"
New-Item -Path $cache -ItemType directory -Force | Out-Null
choco config set cacheLocation $cache

# Set power options to prevent installs from timing out
powercfg -change -monitor-timeout-ac 0 | Out-Null
powercfg -change -monitor-timeout-dc 0 | Out-Null
powercfg -change -disk-timeout-ac 0 | Out-Null
powercfg -change -disk-timeout-dc 0 | Out-Null
powercfg -change -standby-timeout-ac 0 | Out-Null
powercfg -change -standby-timeout-dc 0 | Out-Null
powercfg -change -hibernate-timeout-ac 0 | Out-Null
powercfg -change -hibernate-timeout-dc 0 | Out-Null

Write-Host "[+] Checking for configuration file..."
$configPath = Join-Path $desktopPath "config.xml"
if ([string]::IsNullOrEmpty($customConfig)) {
    Write-Host "[+] Using github configuration file..."
    $configSource = 'https://raw.githubusercontent.com/mandiant/flare-vm/main/config.xml'
} else {
    Write-Host "[+] Using custom configuration file..."
    $configSource = $customConfig
}

Get-ConfigFile $configPath $configSource

Write-Host "Configuration file path: $configPath"

# Check the configuration file exists
if (-Not (Test-Path $configPath)) {
    Write-Host "`t[!] Configuration file missing: " $configPath -ForegroundColor Red
    Write-Host "`t[-] Please download config.xml from $configPathUrl to your desktop" -ForegroundColor Yellow
    Write-Host "`t[-] Is the file on your desktop? (Y/N): " -ForegroundColor Yellow -NoNewline
    $response = Read-Host
    if ($response -notin @("y","Y")) {
        exit 1
    }
    if (-Not (Test-Path $configPath)) {
        Write-Host "`t[!] Configuration file still missing: " $configPath -ForegroundColor Red
        Write-Host "`t[!] Exiting..." -ForegroundColor Red
        Start-Sleep 3
        exit 1
    }
}

# Get config contents
Start-Sleep 1
$configXml = [xml](Get-Content $configPath)



#########################################################################
# GUI Functions
#########################################################################

function Get-Folder($textBox, $envVar) {
	$folderBrowserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
	$folderBrowserDialog.RootFolder = 'MyComputer'
	if ($folderBrowserDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
		$textbox.text = (Join-Path $folderBrowserDialog.SelectedPath (Split-Path $envs[$envVar] -Leaf))
	}
}

# Function that accesses MyGet vm-packages API URL to process packages that are the latest version and have a category
# Saves vm-packages.xml into disk and follows the link after the </entry> tag to retrieve a new version of the XML file
# Returns $packagesByCategory, a hashtable of arrays, where each entry is a PSCustomObject
function Get-Packages-Categories {
   # MyGet API URL that contains a filter to display only the latest packages
   # This URL displays the last two versions of a package
   # Minimize the number of HTTP requests to display all the packages due to the number of versions a package might have
   $vmPackagesUrl = "https://www.myget.org/F/vm-packages/api/v2/Packages?$filter=IsLatestVersion%20eq%20true"
   $vmPackagesFile = "${Env:VM_COMMON_DIR}\vm-packages.xml"
   $packagesByCategory=@{}
   do {
	  # Download the XML from MyGet API
	  Save-FileFromUrl -fileSource $vmPackagesUrl -fileDestination $vmPackagesFile --exitOnError

	  # Load the XML content
	  [xml]$vm_packages = Get-Content $vmPackagesFile

	  # Define the namespaces defined in vm-packages.xml to access nodes
  # Each package resides in the entry node that is defined in the dataservices namespace
	  # Each node has properties that are defined in the metadata namespace
	  $ns = New-Object System.Xml.XmlNamespaceManager($vm_packages.NameTable)
	  $ns.AddNamespace("atom", "http://www.w3.org/2005/Atom")
	  $ns.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices")
	  $ns.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata")

	  # Extract package information from the XML
	  $vm_packages.feed.entry | ForEach-Object {
		 $isLatestVersion = $_.SelectSingleNode("m:properties/d:IsLatestVersion", $ns).InnerText
		 $category = $_.SelectSingleNode("m:properties/d:Tags", $ns).InnerText
		 # Select only packages that have the latest version, contain a category and the category is not excluded
		 if (($isLatestVersion -eq "true") -and ($category -ne "") -and ($excludedCategories -notcontains $category)) {
	            $packageName = $_.properties.Id
				$description = $_.properties.Description
				$projectUrl = $_.properties.projectUrl

				# Initialize category as an empty array
				if (-not ($packagesByCategory.ContainsKey($category))) {
					 $packagesByCategory[$category] = @()
				}
				$packageObject = [PSCustomObject]@{
				PackageName        = $packageName
				PackageDescription = $description
				}
				# Check if $projectUrl contains a valid URL
				if ($projectUrl -match "^http") {
					Add-Member -InputObject $packageObject -MemberType NoteProperty -Name "PackageUrl" -Value $projectURl
				}
				# Add the PackageName and PackageDescription (and PackageUrl if present) to each entry in the array
				$packagesByCategory[$category] += $packageObject
            }
		  }
	  # Check if there is a next link in the XML and set the API URL to that link if it exists
	  $nextLink = $vm_packages.SelectSingleNode("//atom:link[@rel='next']/@href", $ns)
	  $vmPackagesUrl = $nextLink."#text"

   } while ($vmPackagesUrl)

  return $packagesByCategory
}

# Function that returns an array of all the packages that are displayed sorted by category from $packagesByCategory
function Get-AllPackages{
	$listedPackages = $packagesByCategory.Values | ForEach-Object { $_ } | Select-Object -ExpandProperty PackageName
	return $listedPackages
}

# Function that returns additional packages from the config that are not displayed in the textboxes
# which includes both Choco packages and packages from excluded categories
function Get-AdditionalPackages{
   $additionalPackages=@()

   # Packages from the config that are not displayed
   $additionalPackages = $packagesToInstall | where-Object { $listedPackages -notcontains $_}
   return $additionalPackages
}

if (-not $noGui.IsPresent) {

	if ($script:checksPassed -or $noChecks.IsPresent) {
        Write-Host "[+] Beginning graphical install"

		# Gather lists of packages
		$envs = [ordered]@{}
		$configXml.config.envs.env.ForEach({ $envs[$_.name] = $_.value })
		$excludedCategories=@('Command and Control','Credential Access','Exploitation','Forensic','Lateral Movement', 'Payload Development','Privilege Escalation','Reconnaissance','Wordlists','Web Application')
		# Read packages to install from the config
		$packagesToInstall = $configXml.config.packages.package.name
		$packagesByCategory = Get-Packages-Categories
		$listedPackages = Get-AllPackages
		$additionalPackages = Get-AdditionalPackages

        $vmCommonDirText.text            = $envs['VM_COMMON_DIR']
		$rawToolsDirText.text            = $envs['RAW_TOOLS_DIR']
		$toolListDirText.text            = $envs['TOOL_LIST_DIR']

		$Result = $formEnv.ShowDialog()

		if ($Result -eq [System.Windows.Forms.DialogResult]::OK) {
			# Remove default environment variables
			$nodes = $configXml.SelectNodes('//config/envs/env')
			foreach($node in $nodes) {
				$node.ParentNode.RemoveChild($node) | Out-Null
			}

			# Add environment variables
			$envs = $configXml.SelectSingleNode('//envs')
			$newXmlNode = $envs.AppendChild($configXml.CreateElement("env"))
			$newXmlNode.SetAttribute("name", "VM_COMMON_DIR")
			$newXmlNode.SetAttribute("value", $vmCommonDirText.text);
			$newXmlNode = $envs.AppendChild($configXml.CreateElement("env"))
			$newXmlNode.SetAttribute("name", "TOOL_LIST_DIR")
			$newXmlNode.SetAttribute("value", $toolListDirText.text);
			$newXmlNode = $envs.AppendChild($configXml.CreateElement("env"))
			$newXmlNode.SetAttribute("name", "RAW_TOOLS_DIR")
			$newXmlNode.SetAttribute("value", $rawToolsDirText.text)

			[void]$formEnv.Close()

		} else {
			Write-Host "[+] Cancel pressed, stopping installation..."
			Start-Sleep 3
			exit 1
		}

		################################################################################
		## PACKAGE SELECTION BY CATEGORY
		################################################################################

		# Function that adds the selected packages to the config.xml for the installation
		function Install-Selected-Packages{
		  $selectedPackages  = @()
		  $packages = $configXml.SelectSingleNode('//packages')

		  # Remove all child nodes inside <packages>
		  while ($packages.HasChildNodes) {
			$packages.RemoveChild($packages.FirstChild)
		  }

		  foreach ($checkBox in $checkboxesPackages){
			if ($checkBox.Checked){
				$package = $checkbox.Text.split(":")[0]
				$selectedPackages += $package
			}
		  }

		  foreach ($package in $additionalPackagesBox.Items){
			 $selectedPackages += $package
		  }
		  # Add selected packages
		  foreach($package in $selectedPackages) {
			   $newXmlNode = $packages.AppendChild($configXml.CreateElement("package"))
			   $newXmlNode.SetAttribute("name", $package)
		  }
		}

		# Function that resets the checkboxes to match the config.xml
		function Set-InitialPackages {
			foreach ($checkBox in $checkboxesPackages){
				$package =$checkbox.Text.split(":")[0]
				if (($checkbox.Checked) -and ($package -notin $packagesToInstall)){
					$checkBox.Checked = $false
				}else{
				  if ((-not $checkbox.Checked ) -and ($package -in $packagesToInstall)){
					 $checkBox.Checked = $true
				  }
				}
			}
		}
		# Function that returns an array of packages that belong to a specific category
		function Get-PackagesByCategory{
			param (
			 [string]$category
			)
			return $packagesByCategory[$category]
		}

		# Function that returns additional packages from the config that are not displayed in the textboxes
		# which includes both Choco packages and packages from excluded categories
		function Get-AdditionalPackages{
		   $additionalPackages=@()

		   # Packages from the config that are not displayed
		   $additionalPackages = $packagesToInstall | where-Object { $listedPackages -notcontains $_}
		   return $additionalPackages
		}

		# Function that checks all the checkboxes
		function Select-AllPackages {
			foreach ($checkBox in $checkboxesPackages){
				$checkBox.Checked = $true
			}
		}

		# Function that unchecks all the checkboxes
		function Clear-AllPackages {
			foreach ($checkBox in $checkboxesPackages){
				$checkBox.Checked = $false
			}
			$additionalPackagesBox.Items.clear()
		}

		# Function that adds a new package to the listBox of additional packages
		# If the package already exists it returns $false
		function Add-NewPackage {
			param (
			[Parameter(Mandatory=$true)]
			[string]$packageName
			)
			#$packageName = $packageName.Trim()
			$packageName = $packageName -replace '^\s+|\s+$', ''
			if ($packageName -notin $additionalPackagesBox.Items){
			   $additionalPackagesBox.Items.Add($packageName) | Out-Null
			   return $true
			}
			else{
			   return $false
			}

		}

		function Get-ChocoPackage {
			param (
			[Parameter(Mandatory=$true)]
			[string]$PackageName
			)

			choco search $PackageName -e -r | ForEach-Object {
				$Name, $Version = $_ -split '\|'
				New-Object -TypeName psobject -Property @{
					'Name' = $Name
					'Version' = $Version
				}
			}
		}

		 function Get-VMPackage {
			param (
			[Parameter(Mandatory=$true)]
			[string]$PackageName
			)
			if ($PackageName -notlike "*.vm") {
				$PackageName = $PackageName + ".vm"
			}
			choco search $PackageName --exact -r -s "https://www.myget.org/F/vm-packages/api/v2" | ForEach-Object {
				$Name, $Version = $_ -split '\|'
				New-Object -TypeName psobject -Property @{
					'Name' = $Name
					'Version' = $Version
				}
			}
		}

		function Set-AdditionalPackages {
			$additionalPackagesBox.Items.Clear()
			foreach($package in $additionalPackages)
			{
				$additionalPackagesBox.Items.Add($package) | Out-Null
			}
		}

		function Remove-SelectedPackages {
			$additionalPackagesBox.BeginUpdate()
			while ($additionalPackagesBox.SelectedItems.count -gt 0) {
				$additionalPackagesBox.Items.RemoveAt($additionalPackagesBox.SelectedIndex)
			}
			$additionalPackagesBox.EndUpdate()
		}

		Add-Type -AssemblyName System.Windows.Forms
		[System.Windows.Forms.Application]::EnableVisualStyles()

		$formCategories                            = New-Object system.Windows.Forms.Form
		$formCategories.ClientSize                 = New-Object System.Drawing.Point(1015,850)
		$formCategories.text                       = "FLARE-VM Package selection"
		$formCategories.StartPosition              = 'CenterScreen'
		$formCategories.TopMost                    = $true

		if ([string]::IsNullOrEmpty($customConfig)) {
			$textLabel = "The default configuration (recommended) is pre-selected. Click on the reset button to restore the default configuration."
		} else {
			$textLabel = "The provided custom configuration is pre-selected. Click on the reset button to restore the custom configuration."
		}

		$labelCategories                = New-Object system.Windows.Forms.Label
		$labelCategories.text           = "Select packages to install"
		$labelCategories.AutoSize       = $true
		$labelCategories.width          = 25
		$labelCategories.height         = 10
		$labelCategories.location       = New-Object System.Drawing.Point(30,20)
		$labelCategories.Font           = New-Object System.Drawing.Font('Microsoft Sans Serif',10,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))


		$labelCategories2                = New-Object system.Windows.Forms.Label
		$labelCategories2.text           = $textLabel
		$labelCategories2.AutoSize       = $true
		$labelCategories2.location       = New-Object System.Drawing.Point(30,40)
		$labelCategories2.Font           = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

		$panelCategories                = New-Object system.Windows.Forms.Panel
		$panelCategories.height         = 530
		$panelCategories.width          = 970
		$panelCategories.location       = New-Object System.Drawing.Point(30,60)
		$panelCategories.AutoScroll     = $true

		$resetButton                 = New-Object system.Windows.Forms.Button
		$resetButton.text            = "Reset"
		$resetButton.AutoSize        = $true
		$resetButton.location        = New-Object System.Drawing.Point(50,800)
		$resetButton.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$resetButton.Add_Click({
						Set-InitialPackages
						Set-AdditionalPackages
					})

		$allPackagesButton                 = New-Object system.Windows.Forms.Button
		$allPackagesButton.text            = "Select All"
		$allPackagesButton.AutoSize        = $true
		$allPackagesButton.location        = New-Object System.Drawing.Point(130,800)
		$allPackagesButton.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$allPackagesButton.Add_Click({
		   [System.Windows.Forms.MessageBox]::Show('Selecting all packages considerable increases installation time and it is not desirable for most use cases','Warning')
		   Select-AllPackages
		})

		$clearPackagesButton	         = New-Object system.Windows.Forms.Button
		$clearPackagesButton.text            = "Clear"
		$clearPackagesButton.AutoSize        = $true
		$clearPackagesButton.location        = New-Object System.Drawing.Point(210,800)
		$clearPackagesButton.Font            = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$clearPackagesButton.Add_Click({Clear-AllPackages})

		$installButton            = New-Object system.Windows.Forms.Button
		$installButton.text       = "Install"
		$installButton.width      = 97
		$installButton.height     = 37
		$installButton.DialogResult   = [System.Windows.Forms.DialogResult]::OK
		$installButton.location   = New-Object System.Drawing.Point(750,800)
		$installButton.Font       = New-Object System.Drawing.Font('Microsoft Sans Serif',12)

		$cancelButton            = New-Object system.Windows.Forms.Button
		$cancelButton.text       = "Cancel"
		$cancelButton.width      = 97
		$cancelButton.height     = 37
		$cancelButton.location   = New-Object System.Drawing.Point(850,800)
		$cancelButton.Font       = New-Object System.Drawing.Font('Microsoft Sans Serif',12)
		$cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel

		$formCategories.AcceptButton = $installButton
		$formCategories.CancelButton = $cancelButton

		# Create checkboxes for each package
		$checkboxesPackages = New-Object System.Collections.Generic.List[System.Object]
		# Initial vertical position for checkboxes
		$verticalPosition = 25
		$numCheckBoxPackages = 1
		$packages = @()
		foreach ($category in $packagesByCategory.Keys |Sort-Object) {
			# Create Labels for categories
			$labelCategory = New-Object System.Windows.Forms.Label
			$labelCategory.Text = $category
			$labelCategory.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',11,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))
			$labelCategory.AutoSize = $true
			$labelCategory.Location = New-Object System.Drawing.Point(10, $verticalPosition)
			$panelCategories.Controls.Add($labelCategory)

			$NumPackages = 0
			$verticalPosition2 = $verticalPosition + 20
			$packages= Get-PackagesByCategory -category $category
			foreach ($package in $packages)
			{
				$NumPackages++
				$checkBox = New-Object System.Windows.Forms.CheckBox
				$checkBox.Text = $package.PackageName + ": " + $package.PackageDescription
				$checkBox.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
				$checkBox.AutoSize = $true
				$checkBox.Location = New-Object System.Drawing.Point(10, $verticalPosition2)
				$checkBox.Name = "checkBox$numCheckBoxPackages"
				$checkboxesPackages.Add($checkBox)
				$panelCategories.Controls.Add($checkBox)
			    $url = $package.PackageUrl
				if ($url){
					$linkProjectUrl = New-Object System.Windows.Forms.linkLabel
					$linkProjectUrl.Top = $checkbox.Top + 2
					$linkProjectUrl.Left = $checkbox.Right - 3
					$linkProjectUrl.AutoSize                    = $true
					$linkProjectUrl.Font                        = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
					$linkProjectUrl.LinkColor                   = "BLUE";
					$linkProjectUrl.ActiveLinkColor             = "RED"
					$linkProjectUrl.Text                        = "Link"
					$linkProjectUrl.Links.Add(0, 4, $url)| Out-Null
					$linkProjectUrl.add_Click({ Start-Process $this.Links.LinkData })
					$panelCategories.Controls.Add($linkProjectUrl)
				}
				$verticalPosition2 += 20
				$numCheckBoxPackages ++
			}
				# Increment to space checkboxes vertically
			$verticalPosition += 20 * ($NumPackages ) + 30
			$numCategories ++
		}

		# Create empty label and add it to the form categories to add some space
		$posEnd = $verticalPosition2 +10
		$emptyLabel                = New-Object system.Windows.Forms.Label
		$emptyLabel.Width = 20
		$emptyLabel.Height = 10
		$emptyLabel.location       = New-Object System.Drawing.Point(10,$posEnd)
		$panelCategories.Controls.Add($emptyLabel)

		# Select packages that are in the config.xml
		Set-InitialPackages

		$additionalPackagesLabel                          = New-Object system.Windows.Forms.Label
		$additionalPackagesLabel.text                     = "Additional packages to install"
		$additionalPackagesLabel.AutoSize                 = $true
		$additionalPackagesLabel.width                    = 25
		$additionalPackagesLabel.height                   = 10
		$additionalPackagesLabel.location                 = New-Object System.Drawing.Point(30,615)
		$additionalPackagesLabel.Font                     = New-Object System.Drawing.Font('Microsoft Sans Serif',10,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$additionalPackagesBox                 = New-Object system.Windows.Forms.ListBox
		$additionalPackagesBox.text            = "listBox"
		$additionalPackagesBox.SelectionMode   = 'MultiSimple'
		$additionalPackagesBox.Sorted          = $true
		$additionalPackagesBox.width           = 130
		$additionalPackagesBox.height          = 140
		$additionalPackagesBox.location        = New-Object System.Drawing.Point(50,640)

		$deletePackageButton          = New-Object system.Windows.Forms.Button
		$deletePackageButton.text     = "-"
		$deletePackageButton.width    = 24
		$deletePackageButton.height   = 22
		$deletePackageButton.enabled   = $true
		$deletePackageButton.location  = New-Object System.Drawing.Point(190,670)
		$deletePackageButton.Font      = New-Object System.Drawing.Font('Microsoft Sans Serif',12,[System.Drawing.FontStyle]::Bold)
		$deletePackageButton.Add_Click({Remove-SelectedPackages})

		$packageLabel                          = New-Object system.Windows.Forms.Label
		$packageLabel.text                     = "FLARE-VM uses Chocolatey packages. You can add additional packages from:"
		$packageLabel.width                    = 260
		$packageLabel.height                   = 35
		$packageLabel.AutoSize                 = $true
		$packageLabel.location                 = New-Object System.Drawing.Point(300,640)
		$packageLabel.Font                     = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

		$labelChoco                             = New-Object System.Windows.Forms.Label
		$labelChoco.Location                    = New-Object System.Drawing.Point(300,660)
		$labelChoco.Size                        = New-Object System.Drawing.Size(280,20)
		$labelChoco.AutoSize                    = $true
		$labelChoco.Font                        = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$labelChoco.Text                        = "Community Packages"

		$linkLabelChoco                             = New-Object System.Windows.Forms.linkLabel
		$linkLabelChoco.Location                    = New-Object System.Drawing.Point(440,660)
		$linkLabelChoco.AutoSize                    = $true
		$linkLabelChoco.Font                        = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$linkLabelChoco.LinkColor                   = "BLUE"
		$linkLabelChoco.ActiveLinkColor             = "RED"
		$linkLabelChoco.Text                        = "https://community.chocolatey.org/packages"
		$linkLabelChoco.add_Click({Start-Process "https://community.chocolatey.org/packages"})

		$labelFlarevm                             = New-Object System.Windows.Forms.Label
		$labelFlarevm.Location                    = New-Object System.Drawing.Point(300,680)
		$labelFlarevm.Size                        = New-Object System.Drawing.Size(280,20)
		$labelFlarevm.AutoSize                     = $true
		$labelFlarevm.Font                        = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$labelFlarevm.Text                        = "FLARE-VM Packages"

		$linkLabelFlarevm                             = New-Object System.Windows.Forms.linkLabel
		$linkLabelFlarevm.Location                    = New-Object System.Drawing.Point(440,680)
		$linkLabelFlarevm.AutoSize                    = $true
		$linkLabelFlarevm.Font                        = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$linkLabelFlarevm.LinkColor                   = "BLUE"
		$linkLabelFlarevm.ActiveLinkColor             = "RED"
		$linkLabelFlarevm.Text                        = "https://github.com/mandiant/VM-Packages/wiki/Packages"
		$linkLabelFlarevm.add_Click({Start-Process "https://github.com/mandiant/VM-Packages/wiki/Packages"})

		Set-AdditionalPackages

		$chocoPackageLabel                          = New-Object system.Windows.Forms.Label
		$chocoPackageLabel.text                     = "Enter package name:"
		$chocoPackageLabel.AutoSize                 = $true
		$chocoPackageLabel.width                    = 25
		$chocoPackageLabel.height                   = 10
		$chocoPackageLabel.location                 = New-Object System.Drawing.Point(300,715)
		$chocoPackageLabel.Font                     = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

		$packageTextBox                        = New-Object system.Windows.Forms.TextBox
		$packageTextBox.multiline              = $false
		$packageTextBox.width                  = 210
		$packageTextBox.height                 = 20
		$packageTextBox.location               = New-Object System.Drawing.Point(300,735)
		$packageTextBox.Font                   = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$packageTextBox.Add_TextChanged({
				  if ($addPackageButton.Enabled -eq $true){
					  $addPackageButton.Enabled = $false
				  }
		})

		$chocoPackageErrorLabel                          = New-Object system.Windows.Forms.Label
		$chocoPackageErrorLabel.text                     = ""
		$chocoPackageErrorLabel.AutoSize                 = $true
		$chocoPackageErrorLabel.visible                  = $false
		$chocoPackageErrorLabel.width                    = 25
		$chocoPackageErrorLabel.height                   = 10
		$chocoPackageErrorLabel.location                 = New-Object System.Drawing.Point(300,765)
		$chocoPackageErrorLabel.Font                     = New-Object System.Drawing.Font('Microsoft Sans Serif',10,[System.Drawing.FontStyle]([System.Drawing.FontStyle]::Bold))

		$findPackageButton          = New-Object system.Windows.Forms.Button
		$findPackageButton.text     = "Find Package"
		$findPackageButton.width    = 118
		$findPackageButton.height   = 30
		$findPackageButton.enabled   = $true
		$findPackageButton.location  = New-Object System.Drawing.Point(520,730)
		$findPackageButton.Font      = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$findPackageButton.Add_Click({
			$chocoPackageErrorLabel.Visible = $true
			$chocoPackageErrorLabel.text = "Finding package ..."
			$vmPackage = Get-VMPackage -PackageName $packageTextBox.Text.Trim()
			if ($vmPackage){
				$packageName = $vmPackage | Select-Object -ExpandProperty Name
				$chocoPackageErrorLabel.text = "Found VM package"
				$chocoPackageErrorLabel.ForeColor = $successColor
				$packageTextBox.Text = $packageName
				$addPackageButton.enabled = $true
			} else {
				$chocoPackage = Get-ChocoPackage -PackageName $packageTextBox.Text
				if ($chocoPackage) {
				   $chocoPackageErrorLabel.text = "Found Choco package"
				   $chocoPackageErrorLabel.ForeColor = $successColor
				   $addPackageButton.enabled = $true
				} else {
				   $chocoPackageErrorLabel.text = "Package not found"
				   $chocoPackageErrorLabel.ForeColor = $errorColor
				   $addPackageButton.enabled = $false
				}
			}
		})

		$addPackageButton          = New-Object system.Windows.Forms.Button
		$addPackageButton.text     = "Add Package"
		$addPackageButton.width    = 118
		$addPackageButton.height   = 30
		$addPackageButton.enabled   = $false
		$addPackageButton.location  = New-Object System.Drawing.Point(650,730)
		$addPackageButton.Font      = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
		$addPackageButton.Add_Click({
					  if (Add-NewPackage -PackageName $packageTextBox.Text){
						  $chocoPackageErrorLabel.ForeColor = $successColor
						  $chocoPackageErrorLabel.text = "Package added"
					  }else {
						  $chocoPackageErrorLabel.ForeColor = $errorColor
						  $chocoPackageErrorLabel.text = "Error to add the package: duplicated"
					  }
					  $addPackageButton.enabled = $false
			  })

		$formCategories.controls.AddRange(@($additionalPackagesLabel,$packageLabel,$labelChoco,$labelFlarevm,$linkLabelChoco,$linkLabelFlarevm,$linkLabelFlarevm,$additionalPackagesBox,$deletePackageButton,$chocoPackageButton,$chocoPackageLabel,$packageTextBox,$chocoPackageErrorLabel,$findPackageButton,$addPackageButton))
		$formCategories.controls.AddRange(@($labelCategories,$labelCategories2,$panelCategories,$installButton,$resetButton,$allPackagesButton,$cancelButton,$clearPackagesButton))
		$formCategories.Add_Shown({$formCategories.Activate()})
		$resultCategories = $formCategories.ShowDialog()
		if ($resultCategories -eq [System.Windows.Forms.DialogResult]::OK){
			Install-Selected-Packages
		} else {
			Write-Host "[+] Cancel pressed, stopping installation..."
			Start-Sleep 3
			exit 1
		}
	}
		################################################################################
		## END GUI
		################################################################################
}

# Save the config file
Write-Host "[+] Saving configuration file..."
$configXml.save($configPath)

# Parse config and set initial environment variables
Write-Host "[+] Parsing configuration file..."
foreach ($env in $configXml.config.envs.env) {
    $path = [Environment]::ExpandEnvironmentVariables($($env.value))
    Write-Host "`t[+] Setting %$($env.name)% to: $path" -ForegroundColor Green
    [Environment]::SetEnvironmentVariable("$($env.name)", $path, "Machine")
    [Environment]::SetEnvironmentVariable('VMname', 'FLARE-VM', [EnvironmentVariableTarget]::Machine)
}
refreshenv

# Install the common module
# This creates all necessary folders based on custom environment variables
Write-Host "[+] Installing shared module..."
choco install common.vm -y --force
refreshenv

# Use single config
$configXml.save((Join-Path ${Env:VM_COMMON_DIR} "config.xml"))
$configXml.save((Join-Path ${Env:VM_COMMON_DIR} "packages.xml"))

# Custom Start Layout setup
Write-Host "[+] Checking for custom Start Layout file..."
$layoutPath = Join-Path "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell" "LayoutModification.xml"
if ([string]::IsNullOrEmpty($customLayout)) {
    $layoutSource = 'https://raw.githubusercontent.com/mandiant/flare-vm/main/LayoutModification.xml'
} else {
    $layoutSource = $customLayout
}

Get-ConfigFile $layoutPath $layoutSource

# Log basic system information to assist with troubleshooting
Write-Host "[+] Logging basic system information to assist with any future troubleshooting..."
Import-Module "${Env:VM_COMMON_DIR}\vm.common\vm.common.psm1" -Force -DisableNameChecking
VM-Get-Host-Info

Write-Host "[+] Installing the debloat.vm debloater and performance package"
choco install debloat.vm -y --force

# Download FLARE VM background image
$backgroundImage = "${Env:VM_COMMON_DIR}\background.png"
Save-FileFromUrl -fileSource 'https://raw.githubusercontent.com/mandiant/flare-vm/main/Images/flarevm-background.png' -fileDestination $backgroundImage
# Use background image for lock screen as well
$lockScreenImage = "${Env:VM_COMMON_DIR}\lockscreen.png"
Copy-Item $backgroundImage $lockScreenImage

if (-not $noWait.IsPresent) {
    # Show install notes and wait for timeout
    function Wait-ForInstall ($seconds) {
        $doneDT = (Get-Date).AddSeconds($seconds)
        while($doneDT -gt (Get-Date)) {
            $secondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds
            $percent = ($seconds - $secondsLeft) / $seconds * 100
            Write-Progress -Activity "Please read install notes on console below" -Status "Beginning install in..." -SecondsRemaining $secondsLeft -PercentComplete $percent
            [System.Threading.Thread]::Sleep(500)
        }
        Write-Progress -Activity "Waiting" -Status "Beginning install..." -SecondsRemaining 0 -Completed
    }

    Write-Host @"
[!] INSTALL NOTES - PLEASE READ CAREFULLY [!]

- This install is not 100% unattended. Please monitor the install for possible failures. If install
fails, you may restart the install by re-running the install script with the following command:

    .\install.ps1 -password <password> -noWait -noGui -noChecks

- You can check which packages failed to install by listing the C:\ProgramData\chocolatey\lib-bad
directory. Failed packages are stored by folder name. You may attempt manual installation with the
following command:

    choco install -y <package_name>

- For any issues, please submit to GitHub:

    Installer related: https://github.com/mandiant/flare-vm
    Package related:   https://github.com/mandiant/VM-Packages

[!] Please copy this note for reference [!]
"@ -ForegroundColor Red -BackgroundColor White
    Wait-ForInstall -seconds 30
}

# Begin the package install
Write-Host "[+] Beginning install of configured packages..." -ForegroundColor Green
$PackageName = "installer.vm"
if ($noPassword.IsPresent) {
    Install-BoxstarterPackage -packageName $PackageName
} else {
    Install-BoxstarterPackage -packageName $PackageName -credential $credentials
}



================================================
FILE: scripts/lint.ps1
================================================
# Exclude rules that make the code less readable or involve changing the functionality
$excludedRules = "PSAvoidUsingPlainTextForPassword", "PSAvoidUsingConvertToSecureStringWithPlainText", "PSAvoidUsingWriteHost", "PSUseShouldProcessForStateChangingFunctions", "PSUseSingularNouns", "PSAvoidUsingInvokeExpression"

choco install psscriptanalyzer --version 1.23.0 --no-progress

# Manually iterate over all files instead of using -Recurse because
# PSScriptAnalyzer only outputs the script name (and most have the name
# chocolateyinstall.ps1)
$scripts = Get-ChildItem . -Filter *.ps*1 -Recurse -File -Name
$errorsCount = 0
foreach ($script in $scripts) {
  Write-Host -ForegroundColor Yellow $script
  ($errors = Invoke-ScriptAnalyzer $script -Recurse -ReportSummary -ExcludeRule $excludedRules)
  $errorsCount += $errors.Count
}

Exit($errorsCount)


================================================
FILE: virtualbox/README.md
================================================
# VirtualBox scripts

**This folder contains several scripts related to enhance building, exporting, and using FLARE-VM in VirtualBox.**
The scripts have been tested in Debian 12 with GNOME 44.9.


## Clean up snapshots

It is not possible to select and delete several snapshots in VirtualBox, making cleaning up your virtual machine (VM) manually after having creating a lot snapshots time consuming and tedious (possible errors when deleting several snapshots simultaneously).

[`vbox-clean-snapshots.py`](vbox-clean-snapshots.py) cleans a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name.

### Example

```
$ ./vbox-remove-snapshots.py FLARE-VM.20240604 --protected empty,clean,done,important

Snapshots with the following strings in the name (case insensitive) won't be deleted:
  clean
  done

Cleaning FLARE-VM.20240604 🫧 Snapshots to delete:
  Snapshot 1
  wip unpacked
  JS downloader deobfuscated 
  Snapshot 6
  C2 decoded
  Snapshot 5
  wip
  Snapshot 4
  Snapshot 3
  Snapshot 2
  complicated chain - all samples ready

VM state: Paused
⚠️  Snapshot deleting is slower in a running VM and may fail in a changing state

Confirm deletion (press 'y'):y

Deleting... (this may take some time, go for an 🍦!)
  🫧 DELETED 'Snapshot 1'
  🫧 DELETED 'wip unpacked'
  🫧 DELETED 'JS downloader deobfuscated '
  🫧 DELETED 'Snapshot 6'
  🫧 DELETED 'C2 decoded'
  🫧 DELETED 'Snapshot 5'
  🫧 DELETED 'wip'
  🫧 DELETED 'Snapshot 4'
  🫧 DELETED 'Snapshot 3'
  🫧 DELETED 'Snapshot 2'
  🫧 DELETED 'complicated chain - all samples ready'

See you next time you need to clean up your VMs! ✨

```

##### Before

![Before](../Images/vbox-clean-snapshots_before.png)

##### After

![After](../Images/vbox-clean-snapshots_after.png)


## Check internet adapter status

[`vbox-adapter-check.py`](vbox-adapter-check.py) prints the status of all internet adapters of all VMs in VirtualBox.
If the argument `--dynamic_only` is provided, the script only print the status of the dynamic analysis VM (with `.dynamic` in the name).
Unless the argument `--do_not_modify` is provided, if internet is detected in any dynamic analysis VM, the script sends a notification and changes the adapters type to Host-Only.
The script is useful to detect internet access, which is undesirable for dynamic malware analysis.

### Example

```
$ ~/github/flare-vm/virtualbox/vbox-adapter-check.py
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ⚠️  FLARE-VM.testing is connected to the internet on adapter(s): 1
VM {a23c0c37-2062-4cf0-882b-9e9747dd33b6} ✅ REMnux.20241217.dynamic network configuration is ok
VM {fa0b3733-50cb-43fd-8428-745d0e9159cb} ✅ FLARE-VM.Win10.20250211.dynamic network configuration is ok
VM {e5f509ed-cbc8-4abc-b052-664246207e89} ⚠️  FLARE-VM.Win10.20250211.full.dynamic is connected to the internet on adapter(s): 1, 2
VM {e5f509ed-cbc8-4abc-b052-664246207e89} ⚙️  FLARE-VM.Win10.20250211.full.dynamic set adapter 1 to hostonly
VM {e5f509ed-cbc8-4abc-b052-664246207e89} ⚙️  FLARE-VM.Win10.20250211.full.dynamic set adapter 2 to hostonly
```

#### Notification

![Notification](../Images/vbox-adapter-check_notification.png)


## Export snapshot

[`vbox-export-snapshot.py`](vbox-export-snapshot.py) exports a VirtualBox snapshot as an Open Virtual Appliance (OVA) file.
The script configures the exported VM with a single Host-Only network interface, and the resulting OVA file is named after the snapshot.
A separate file containing the SHA256 hash of the OVA is also generated for verification.
The script accepts an optional description for the OVA and the name of the export directory within the user's home directory (`$HOME`) where the OVA and SHA256 hash file will be saved.
If no export directory is provided, the default directory name is `EXPORTED VMS`.

### Example

```
$ ./vbox-export-snapshots.py "FLARE-VM.testing" "FLARE-VM" --description "Windows 10 VM with FLARE-VM default configuration"

Exporting snapshot "FLARE-VM" from "FLARE-VM.testing" {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d}...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✨ restored snapshot "FLARE-VM"
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: saved. Starting VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ⚙️  network set to single hostonly adapter
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🔄 power cycling before export... (it will take some time, go for an 🍦!)
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: poweroff. Starting VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🚧 exporting ... (it will take some time, go for an 🍦!)
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ EXPORTED "/home/anamg/None/FLARE-VM.ova"
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ GENERATED "/home/anamg/None/FLARE-VM.ova.sha256": 987eed68038ce7c5072e7dc219ba82d11745267d8ab2ea7f76158877c13e3aa9
```

## Build FLARE-VM VM(s)

[`vbox-build-flare-vm.py`](vbox-build-flare-vm.py) automates the creation and export of customized FLARE-VM VMs.
The script begins by restoring a pre-existing `BUILD-READY` snapshot of a clean Windows installation.
The script then copies the required installation files (such as the IDA Pro installer, FLARE-VM configuration, and legal notices) into the guest VM.
After installing FLARE-VM, a `base` snapshot is taken.
This snapshot serves as the foundation for generating subsequent snapshots and exporting OVA images, all based on the configuration provided in a YAML file.
This configuration file specifies the VM name, the exported VM name, and details for each snapshot.
Individual snapshot configurations can include custom commands to be executed within the guest, legal notices to be applied, and file/folder exclusions for the automated cleanup process.
See the configuration example file [`configs/win10_flare-vm.yaml`](configs/win10_flare-vm.yaml).

The `BUILD-READY` snapshot is expected to be an empty Windows installation that satisfies the FLARE-VM installation requirements and has UAC disabled.
To disable UAC execute in a cmd console with admin rights and restart the VM for the change to take effect:
```
%windir%\System32\reg.exe ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
```

## Build REMnux VM

Similarly to [`vbox-build-flare-vm.py`](vbox-build-flare-vm.py), [`vbox-build-remnux.py`](vbox-build-remnux.py) automates the creation and export of customized REMnux virtual machines (VMs).
The script begins by restoring a pre-existing "BUILD-READY" snapshot of a clean REMnux OVA.
Required installation files (such as the IDA Pro installer and ZIPs with GNOME extensions) are then copied into the guest VM.
The configuration file specifies the VM name, the exported VM name, and details for each snapshot.
Individual snapshot configurations include the extension, description, and custom commands to be executed within the guest.
See the configuration example file [`configs/remnux.yaml`](configs/remnux.yaml).


================================================
FILE: virtualbox/configs/remnux.yaml
================================================
VM_NAME: REMnux.testing
EXPORTED_VM_NAME: REMnux
SNAPSHOT:
  extension: ".dynamic"
  description: "REMnux (based on Ubuntu) with improved configuration"
CMDS:
  - |
    # Install additional useful packages
    sudo apt-get --assume-yes install libwrap0-dev gdb-multiarch qemu gcc-multilib libcurl4:i386 qemu-user libc6-mips64-mips-cross libc6-mipsel-cross libc6-arm64-cross libc6-armel-cross libc6-armhf-cross libc6-ppc64-cross libc6-powerpc-cross


  - |
    # Uninstall distro-info to fix "Invalid version: '0.23ubuntu1'" Python install warning
    # Uninstall it in both the default and Python 3.9
    sudo pip uninstall -y distro-info
    sudo /usr/bin/python3.9 -m pip uninstall -y distro-info

  - |
    # Install additional Python libraries
    pip install -U pip
    pip install rpyc flare-capa lznt1

  - |
    # Install additional Python libraries using Python 3.9
    /usr/bin/python3.9 -m pip install -U pip
    /usr/bin/python3.9 -m pip install rpyc flare-capa lznt1

  - |
    # Fix fakenet issue: https://github.com/mandiant/flare-fakenet-ng/tree/master?tab=readme-ov-file#dns-not-resolving-names
    sudo systemctl stop systemd-resolved
    sudo systemctl disable systemd-resolved

  - |
    # Install IDA
    # Expected IDA 9 installer in the Desktop
    cd /home/remnux/Desktop
    sudo chmod +x ida-pro_*.run
    ./ida-pro_*.run --mode unattended

    # Add IDA to favourite apps on startup (/usr/local/share/remnux/gnome-config.sh replaces it on startup)
    ida_app=$(basename /home/remnux/.local/share/applications/com.hex_rays.IDA.pro*.desktop)
    favourite_apps=$(gsettings get org.gnome.shell favorite-apps | sed 's/.$//')
    echo '' | sudo tee -a /usr/local/share/remnux/gnome-config.sh
    echo "gsettings set org.gnome.shell favorite-apps \"$favourite_apps, '$ida_app']\"" | sudo tee -a /usr/local/share/remnux/gnome-config.sh

    # Ensure files are written to persistent storage as the script shut down the VM abruptly
    sync

  - |
    # Install Dash to Panel extension
    # Expected a ZIP with a version for the GNOME shell 3.36 in the Desktop
    cd /home/remnux/Desktop
    gnome-extensions install dash-to-panel*.shell-extension.zip --force

    # Enable Dash to Panel extension on startup as logout is needed after install
    echo gnome-extensions enable dash-to-panel@jderose9.github.com | sudo tee -a /usr/local/share/remnux/gnome-config.sh

    # Ensure files are written to persistent storage as the script shut down the VM abruptly
    sync



================================================
FILE: virtualbox/configs/win10_flare-vm-edu.yaml
================================================
VM_NAME: FLARE-VM.testing
EXPORTED_VM_NAME: FLARE-VM.Win10
SNAPSHOTS:
- extension: ".EDU"
  description: "Windows 10 VM with FLARE-VM default configuration + FLARE-EDU materials"
  cmd: |
    $desktop = "C:\Users\flare\Desktop";
    Set-Location $desktop;

    # Unzip EDU labs
    VM-Unzip-Recursively;

    # Install Office 2016. the installation takes 30 minutes
    $path = "$desktop\en_office_professional_plus_2016_x86_x64_dvd_6962141.iso";
    $drive = (Mount-DiskImage -ImagePath $path | Get-Volume).DriveLetter;
    Set-Location "$drive`:\";
    .\setup.exe;
    Start-Sleep 1800;
    Dismount-DiskImage -ImagePath $path;

    # Ensure files are written to persistent storage as the script shut down the VM abruptly
    # sync is installed by sysinternals
    sync;
  legal_notice: "legal_notice_edu.txt"
  protected_folders: "'ATMA', 'MACC', 'MAF', 'MDA'"
  protected_files: "'Labs.zip', 'MICROSOFT Windows 10 License Terms.txt', 'MICROSOFT Office 2016 License Terms.txt'"


================================================
FILE: virtualbox/configs/win10_flare-vm.yaml
================================================
VM_NAME: FLARE-VM.testing
EXPORTED_VM_NAME: FLARE-VM.Win10
SNAPSHOTS:
- extension: ".dynamic"
  description: "Windows 10 VM with FLARE-VM default configuration + idapro.vm"
  cmd: |
    choco install idapro.vm;

    # Ensure files are written to persistent storage as the script shut down the VM abruptly
    # sync is installed by sysinternals
    sync;
  legal_notice: "legal_notice.txt"
- extension: ".full.dynamic"
  description: "Windows 10 VM with FLARE-VM default configuration + idapro.vm + pdbs.pdbresym.vm + visualstudio.vm"
  cmd: |
    choco install idapro.vm pdbs.pdbresym.vm visualstudio.vm --execution-timeout 10000;

    # Ensure files are written to persistent storage as the script shut down the VM abruptly
    # sync is installed by sysinternals
    sync;
  legal_notice: "legal_notice.txt"


================================================
FILE: virtualbox/configs/win11_flare-vm.yaml
================================================
VM_NAME: FLARE-VM.Win11.testing
EXPORTED_VM_NAME: FLARE-VM.Win11
SNAPSHOTS:
- extension: ".dynamic"
  description: "Windows 11 VM with FLARE-VM default configuration + idapro.vm"
  cmd: |
    choco install idapro.vm;

    # Ensure files are written to persistent storage as the script shut down the VM abruptly
    # sync is installed by sysinternals
    sync;
  legal_notice: "legal_notice.txt"
- extension: ".full.dynamic"
  description: "Windows 11 VM with FLARE-VM default configuration + idapro.vm + pdbs.pdbresym.vm + visualstudio.vm"
  cmd: |
    choco install idapro.vm pdbs.pdbresym.vm visualstudio.vm --execution-timeout 10000;

    # Ensure files are written to persistent storage as the script shut down the VM abruptly
    # sync is installed by sysinternals
    sync;
  legal_notice: "legal_notice.txt"


================================================
FILE: virtualbox/install.sh
================================================
#!/bin/bash

# This script configures the vbox-adapter-check file to run automatically. It performs setup of a cron task.

# --- Configuration ---
INSTALL_DIR="$HOME/vbox"

set -e

echo_step() {
  echo -e "\n\033[1;34m==> $1\033[0m"
}

echo_info() {
  echo "$1"
}

echo_success() {
  echo -e "\033[1;32m✅ $1\033[0m"
}

echo_error() {
  echo -e "\n\033[1;31m❌ ERROR: $1\033[0m" >&2
  exit 1
}

# Step 1: Create installation directory and copy files
echo_step "Setting up installation directory..."
mkdir -p "$INSTALL_DIR"
SCRIPT_DIR=$(dirname "$0")
if [ -f "$SCRIPT_DIR/vbox-adapter-check" -a -f "$SCRIPT_DIR/vbox-clean-snapshots" ]; then
    cp "$SCRIPT_DIR/vbox-adapter-check" "$INSTALL_DIR/"
    cp "$SCRIPT_DIR/vbox-clean-snapshots" "$INSTALL_DIR/"
elif [ -f "vbox-adapter-check" -a -f "vbox-clean-snapshots" ]; then
    cp "vbox-adapter-check" "$INSTALL_DIR/"
    cp "vbox-clean-snapshots" "$INSTALL_DIR/"
else
    echo_error "The 'vbox-adapter-check' and 'vbox-clean-snapshots' binaries are not in the directory of the script or the current directory."
fi
echo_info "Copied 'vbox-adapter-check' and 'vbox-clean-snapshots' to $INSTALL_DIR"

# Step 2: Make files executable
echo_step "Making tools in $INSTALL_DIR executable..."
if ! chmod +x "$INSTALL_DIR"/*; then
  echo_error "Failed to set execute permissions on files in $INSTALL_DIR."
fi
echo_info "File permissions updated."

# Step 3: Run vbox-adapter-check
echo_step "Running vbox-adapter-check"
$INSTALL_DIR/vbox-adapter-check

# Step 4: Schedule the cron job if it doesn't exist
echo_step "Scheduling background task..."
CRON_JOB="*/5 * * * * (echo \"# \$(date)\"; $INSTALL_DIR/vbox-adapter-check) >> \"$INSTALL_DIR/vbox-adapter-check.log\" 2>&1"

# Check if the job already exists
if crontab -l 2>/dev/null | grep -Fq "vbox-adapter-check"; then
    echo_info "Cron job for vbox-adapter-check already exists. Skipping."
else
    echo_info "Adding cron job..."
    (crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab -
    echo_info "Cron job scheduled."
fi

echo_success "Installation Successful!"
echo_info "The vbox tools are installed in: $INSTALL_DIR"
echo_info "vbox-adapter-check writes logging information every 5 minutes to: $INSTALL_DIR/vbox-adapter-check.log"

echo_step "MANUAL ACTION REQUIRED: Add to PATH"
echo_info "To run the 'vbox' commands easily, you must add the installation directory to your shell's PATH."
echo_info "Choose the command for your shell and add it to your startup file (e.g., ~/.bashrc, ~/.zshrc):"
echo ""
echo "    # For bash or zsh shells:"
echo "    echo 'export PATH=\"\$HOME/vbox:\$PATH\"' >> ~/.bashrc  # Or ~/.zshrc"
echo ""
echo "    # For fish shell:"
echo "    echo 'set -U fish_user_paths \$HOME/vbox \$fish_user_paths' >> ~/.config/fish/config.fish"
echo ""
echo_info "After updating your config file, restart your shell or run 'source ~/.bashrc' (or equivalent) to apply the changes."


================================================
FILE: virtualbox/vbox-adapter-check.py
================================================
#!/usr/bin/python3
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import argparse
import re
import sys
import textwrap

import gi
from vboxcommon import ensure_hostonlyif_exists, get_vm_state, run_vboxmanage

gi.require_version("Notify", "0.7")
from gi.repository import Notify  # noqa: E402

DYNAMIC_VM_NAME = ".dynamic"
DISABLED_ADAPTER_TYPE = "hostonly"
ALLOWED_ADAPTER_TYPES = ("hostonly", "intnet", "none")

DESCRIPTION = f"""Print the status of all internet adapters of all VMs in VirtualBox.
Optionally, if any VM with {DYNAMIC_VM_NAME} in the name has an adapter whose type is not allowed,
send a notification and change the type of the adapters with non-allowed type to {DISABLED_ADAPTER_TYPE}.
This is useful to detect internet access which is undesirable for dynamic malware analysis."""


EPILOG = textwrap.dedent(
    f"""
    Example usage:
      # Print status of all interfaces. For the VMs whose name contain {DYNAMIC_VM_NAME},
      # show a notification and disable internet if enabled.
      vbox-adapter-check.vm

      # For the VMs whose name contain {DYNAMIC_VM_NAME}, print the status of their interfaces.
      # If internet is enabled, show a notification and disable internet.
      vbox-adapter-check.vm --dynamic_only

      # Print status of all interfaces without modifying any of them.
      vbox-adapter-check.vm --do_not_modify

      # Print status of all interfaces in VMs whose name contain {DYNAMIC_VM_NAME} without modifying any of them.
      vbox-adapter-check.vm --dynamic_only --do_not_modify
    """
)


def get_vms(dynamic_only):
    """Get the names and UUID of the VirtualBox VMs using 'VBoxManage list vms'.

    Args:
        dynamic_only: If true, only the VMs containing DYNAMIC_VM_NAME in the name are returned.

    Returns:
        A list of tuples, where each tuple contains the VM name (str) and VM UUID (str).
        Returns an empty list if no VMs are found.
    """
    vms_list = []
    # regex VM name and extract the GUID
    # Example of `VBoxManage list vms` output:
    # "FLARE-VM.testing" {b76d628b-737f-40a3-9a16-c5f66ad2cfcc}
    # "FLARE-VM" {a23c0c37-2062-4cf0-882b-9e9747dd33b6}
    vms_info = run_vboxmanage(["list", "vms"])

    vms = re.findall(r'"(.*?)" (\{.*?\})', vms_info)
    for vm_name, vm_uuid in vms:
        # Get only the VMs containing DYNAMIC_VM_NAME in the name if dynamic_only is true
        if not (dynamic_only and (DYNAMIC_VM_NAME in vm_name)):
            vms_list.append((vm_name, vm_uuid))
    return vms_list


def get_nics(vm_uuid, only_nic=None):
    """
    Retrieves the configured network interfaces and their types for a given virtual machine.

    Args:
        vm_uuid: The unique identifier (UUID) of the virtual machine.
        only_nic: An optional string specifying a specific NIC number to retrieve
                  (e.g., "1" for nic1). If None, information for all configured NICs
                  will be returned.

    Returns:
        A list of tuples, where each tuple contains:
        - The NIC number as a string (e.g., "1", "2")
        - The NIC value (e.g., "hostonly", "nat")
    """

    # Example of `VBoxManage showvm_info <VM_UUID> --machinereadable` relevant output:
    # nic1="hostonly"
    # nictype1="82540EM"
    # nicspeed1="0"
    # nic2="none"
    # nic3="none"
    # nic4="none"
    # nic5="none"
    # nic6="none"
    # nic7="none"
    # nic8="none"
    vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])

    # If no nic provided, get all possible numbers using RegExp
    only_nic = r"\d+"

    # Get adapters numbers and their values as a list: [(nic_number, nic_value)]
    return re.findall(rf'^nic({only_nic})="(\S+)"', vm_info, flags=re.M)


def disable_adapter(vm_uuid, nic_number, hostonly_ifname):
    """Disable the network adapter of the VM by setting it to DISABLED_ADAPTER_TYPE

    Args:
        vm_uuid: VM UUID
        nic_number: nic to disable

    Raises:
        RuntimeError: If the nic type is not changed to DISABLED_ADAPTER_TYPE
    """
    # We need to run a different command if the machine is running.
    if get_vm_state(vm_uuid) in ("poweroff", "aborted"):
        run_vboxmanage(
            [
                "modifyvm",
                vm_uuid,
                f"--nic{nic_number}",
                DISABLED_ADAPTER_TYPE,
            ]
        )
        # Set the hostonlyadapter for nic as "VBoxManage modifyvm --nic" does not set it
        # If hostonlyadapter is empty, starting the VM raises an error
        run_vboxmanage(
            [
                "modifyvm",
                vm_uuid,
                f"--hostonlyadapter{nic_number}",
                hostonly_ifname,
            ]
        )
    else:
        run_vboxmanage(
            [
                "controlvm",
                vm_uuid,
                f"nic{nic_number}",
                DISABLED_ADAPTER_TYPE,
                hostonly_ifname,
            ]
        )

    # Verify nic has been modify as the command may return code 0 even if it fails to set the adapter
    _, nic_value = get_nics(vm_uuid, nic_number)[0]
    if nic_value != DISABLED_ADAPTER_TYPE:
        raise RuntimeError(f"nic{nic_number} has type '{nic_value}'")


def list_to_str(string_list):
    """Joins a list of strings with ", "."""
    return ", ".join(string_list)


def verify_network_adapters(vm_uuid, vm_name, hostonly_ifname, modify_and_notify):
    """Verify and optionally correct network adapter configurations for a given VM.

    Check the network adapter types of a given VM against a list of allowed types (`ALLOWED_ADAPTER_TYPES`).
    If not allowed adapter types are found, print a warning and, if `do_not_modify` is False, disable the adapters and sends a desktop notification.

    Args:
        vm_uuid: The unique identifier (UUID) of the VM.
        vm_name: The name of the VM.
        hostonly_ifname: The name of the host-only network interface. This is passed for potential use in
                         disabling adapters (though not directly used in the verification logic).
        modify_and_notify: A boolean flag. If False, invalid adapters will only be reported, without automatic modification and notification.
    """
    try:
        invalid_nics = []
        for nic_number, nic_value in get_nics(vm_uuid):
            if nic_value not in ALLOWED_ADAPTER_TYPES:
                invalid_nics.append(nic_number)

        if not invalid_nics:
            print(f"VM {vm_uuid} ✅ {vm_name} network configuration is ok")
            return

        invalid_nics_msg = list_to_str(invalid_nics)
        print(f"VM {vm_uuid} ⚠️  {vm_name} is connected to the internet on adapter(s): {invalid_nics_msg}")

        if modify_and_notify:
            # Disable invalid nics
            for nic in invalid_nics:
                try:
                    disable_adapter(vm_uuid, nic, hostonly_ifname)
                    print(f"VM {vm_uuid} ⚙️  {vm_name} set adapter {nic} to {DISABLED_ADAPTER_TYPE}")
                except Exception as e:
                    print(f"VM {vm_uuid} ❌ {vm_name} unable to disable adapter {nic}: {e}")

            message = (
                f"{vm_name} may be connected to the internet on adapter(s): {invalid_nics_msg}."
                "The network adapter(s) may have been disabled automatically to prevent an undesired internet connectivity."
                "Please double check your VMs settings."
            )
            # Show notification using PyGObject
            Notify.init("VirtualBox adapter check")
            notification = Notify.Notification.new(f"⚠️  INTERNET IN VM: {vm_name}", message, "dialog-error")
            # Set highest priority
            notification.set_urgency(2)
            notification.show()

    except Exception as e:
        print(f"VM {vm_uuid} {vm_name} ❌ Unable to verify network adapters: {e}")


def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--do_not_modify",
        action="store_true",
        help="Only print the status of the internet adapters without modifying them and without showing a notification.",
    )
    parser.add_argument(
        "--dynamic_only",
        action="store_true",
        help="Only scan VMs with .dynamic in the name",
    )
    args = parser.parse_args(args=argv)

    hostonly_ifname = ensure_hostonlyif_exists()
    vms = get_vms(args.dynamic_only)
    if len(vms) > 0:
        for vm_name, vm_uuid in vms:
            # Never modify VMs without DYNAMIC_VM_NAME in the name (only check the status)
            modify_and_notify = (DYNAMIC_VM_NAME in vm_name) and (not args.do_not_modify)
            verify_network_adapters(vm_uuid, vm_name, hostonly_ifname, modify_and_notify)
    else:
        print("⚠️  No VMs found!")


if __name__ == "__main__":
    main()


================================================
FILE: virtualbox/vbox-build-flare-vm.py
================================================
#!/usr/bin/python3
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import os
import sys
import time
from datetime import datetime

import yaml
from vboxcommon import (
    LONG_WAIT,
    control_guest,
    ensure_vm_running,
    export_vm,
    get_vm_state,
    get_vm_uuid,
    restore_snapshot,
    set_network_to_hostonly,
    take_snapshot,
)

DESCRIPTION = """
Automates the creation and export of customized FLARE-VM virtual machines (VMs).
Begins by restoring a pre-existing "BUILD-READY" snapshot of a clean Windows installation (with UAC disabled).
Required installation files (such as the IDA Pro installer, FLARE-VM configuration, and legal notices) are then copied into the guest VM.
After installing FLARE-VM, a "base" snapshot is taken.
This snapshot serves as the foundation for generating subsequent snapshots and exporting OVA images,
all based on the configuration provided in a YAML file.
This configuration file specifies the VM name, the exported VM name, and details for each snapshot.
Individual snapshot configurations can include custom commands to be executed within the guest, legal notices to be applied,
and file/folder exclusions for the automated cleanup process.
"""

EPILOG = """
Example usage:
  # Build FLARE-VM and export several OVAs using the information in the provided configuration file, using '19930906' as date
  # ./vbox-build-flare-vm.py configs/win10_flare-vm.yaml --custom_config --date='19930906'
"""

# The base snapshot is expected to be an empty Windows installation that satisfies the FLARE-VM installation requirements and has UAC disabled
# To disable UAC execute in a cmd console with admin rights and restart the VM for the change to take effect:
# %windir%\System32\reg.exe ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
BASE_SNAPSHOT = "BUILD-READY"

# Guest username and password, needed to execute commands in the guest
GUEST_USERNAME = "flare"
GUEST_PASSWORD = "password"

# Logs
LOGS_DIR = os.path.expanduser("~/FLARE-VM LOGS")
LOG_FILE_GUEST = r"C:\ProgramData\_VM\log.txt"
LOG_FILE_HOST = rf"{LOGS_DIR}/flare-vm-log.txt"
FAILED_PACKAGES_GUEST = r"C:\ProgramData\_VM\failed_packages.txt"
FAILED_PACKAGES_HOST = rf"{LOGS_DIR}/flare-vm-failed_packages.txt"

# Required files
REQUIRED_FILES_DIR = os.path.expanduser("~/FLARE-VM REQUIRED FILES")
REQUIRED_FILES_DEST = rf"C:\Users\{GUEST_USERNAME}\Desktop"

# Executable paths in guest
POWERSHELL_PATH = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
CMD_PATH = r"C:\Windows\System32\cmd.exe"

# Cleanup command to be executed in cmd to delete the PowerShell logs
# Run sync (installed by sysinternals) to ensure files are written to persistent storage as the script shut down the VM abruptly
CMD_CLEANUP_CMD = r"/C rmdir /s /q %UserProfile%\Desktop\PS_Transcripts && sync"


def run_command(vm_uuid, cmd, executable="PS"):
    """Run a command in the guest of the specified VM, displaying the output in real time to the console.

    Args:
        vm_uuid: VM UUID
        cmd: The command string to execute in the guest.
        executable: Specifies the executable to use for running the command, either `PS` (`powershell.exe) or `CMD` (`cmd.exe`).
    """
    ensure_vm_running(vm_uuid)

    exe_path = POWERSHELL_PATH if executable == "PS" else CMD_PATH

    print(f"VM {vm_uuid} 🚧 {executable}: {cmd}")
    control_guest(vm_uuid, GUEST_USERNAME, GUEST_PASSWORD, ["run", exe_path, cmd], True)


def create_log_folder():
    """Ensure log folder exists and is empty."""
    # Create directory if it does not exist
    os.makedirs(LOGS_DIR, exist_ok=True)
    print(f"Log folder: {LOGS_DIR}\n")

    # Remove all files in the logs directory. Note the directory only files (the logs).
    for file_name in os.listdir(LOGS_DIR):
        file_path = os.path.join(LOGS_DIR, file_name)
        os.remove(file_path)


def install_flare_vm(vm_uuid, snapshot_name, custom_config):
    """Install FLARE-VM"""
    additional_arg = r"-customConfig '$desktop\config.xml'" if custom_config else ""
    flare_vm_installation_cmd = rf"""
    $desktop=[Environment]::GetFolderPath("Desktop")
    cd $desktop
    Set-ExecutionPolicy Unrestricted -Force
    $url="https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1"
    $file = "$desktop\install.ps1"
    (New-Object net.webclient).DownloadFile($url,$file)
    Unblock-File .\install.ps1

    start powershell "$file -password password -noWait -noGui -noChecks {additional_arg}"
    """
    run_command(vm_uuid, flare_vm_installation_cmd)
    print(f"VM {vm_uuid} ✅ FLARE-VM is being installed...{LONG_WAIT}")

    index = 0
    while True:
        time.sleep(120)  # Wait 2 minutes
        try:
            control_guest(
                vm_uuid,
                GUEST_USERNAME,
                GUEST_PASSWORD,
                ["copyfrom", f"--target-directory={FAILED_PACKAGES_HOST}", FAILED_PACKAGES_GUEST],
            )
            break
        except RuntimeError:
            index += 1
            if (index % 10) == 0:  # Print an "I am alive" message every ~20 minutes
                time_str = datetime.now().strftime("%Y-%m-%d %H:%M")
                print(f"VM {vm_uuid} 🕑 {time_str} still waiting")
                # Take snaphost that can be restore if the VM crashes
                # Avoid taking a snapshot during a restart (as it could crash the VM) by checking the VM is running
                if get_vm_state(vm_uuid) == "running":
                    wip_snapshot_name = f"WIP {snapshot_name} {time_str}"
                    take_snapshot(vm_uuid, wip_snapshot_name)

    print(f"VM {vm_uuid} ✅ FLARE-VM installed!")

    control_guest(
        vm_uuid, GUEST_USERNAME, GUEST_PASSWORD, ["copyfrom", f"--target-directory={LOG_FILE_HOST}", LOG_FILE_GUEST]
    )
    print(f"VM {vm_uuid} 📁 Copied FLARE-VM log: {REQUIRED_FILES_DIR}")

    # Read failed packages from log file and print them
    try:
        if os.path.getsize(FAILED_PACKAGES_HOST):
            print("  ❌ FAILED PACKAGES")
            with open(FAILED_PACKAGES_HOST, "r") as f:
                for failed_package in f:
                    print(f"     - {failed_package}")
    except Exception:
        print(f"  ❌ Reading {FAILED_PACKAGES_HOST} failed")


def build_vm(vm_name, exported_vm_name, snapshots, date, custom_config, do_not_install_flare_vm):
    """
    Build and export multiple FLARE-VM VMs as OVAs based on provided configurations.

    This function first prepares a base FLARE-VM VM by restoring a BASE_SNAPSHOT,
    copying necessary files, and installing the FLARE-VM software. A base snapshot
    of this installation is then taken. Subsequently, for each configuration
    specified in the `snapshots` list, the base snapshot is restored, customized
    with specific commands and settings, and then exported as an OVA.

    Args:
        vm_name: The name of the VM.
        exported_vm_name: The base name to use for naming the exported VMs and their snapshots.
        snapshots: A list of dictionaries, where each dictionary defines the configuration for a specific exported VM.
                   Each dictionary can contain the following keys:
                     - cmd: A command to execute in the guest VM.
                     - legal_notice: The filename of a legal notice to set on the VM.
                     - protected_files: A string of files to exclude during the cleanup process.
                     - protected_folders:: A string of folders to exclude during the cleanup process.
                     - extension: An extension to add to the exported VM's filename.
                     - description: A description to embed in the exported OVA.
        date: A date string appended to the names of the base snapshot and exported OVAs.
        custom_config: Custom configuration parameters passed to the FLARE-VM installation script.
        do_not_install_flare_vm: If True, the FLARE-VM installation step is skipped and an existent base snapshot used.
                                 It also does not copy the required files.
    """
    vm_uuid = get_vm_uuid(vm_name)
    if not vm_uuid:
        print(f'❌ ERROR: "{vm_name}" not found')
        exit()

    print(f'\nGetting the installation VM "{vm_name}" {vm_uuid} ready...')
    create_log_folder()

    base_snapshot_name = f"{exported_vm_name}.{date}.base"

    if not do_not_install_flare_vm:
        restore_snapshot(vm_uuid, BASE_SNAPSHOT)

        # Copy required files
        control_guest(
            vm_uuid,
            GUEST_USERNAME,
            GUEST_PASSWORD,
            ["copyto", "--recursive", f"--target-directory={REQUIRED_FILES_DEST}", REQUIRED_FILES_DIR],
        )
        print(f"VM {vm_uuid} 📁 Copied required files in: {REQUIRED_FILES_DIR}")

        install_flare_vm(vm_uuid, exported_vm_name, custom_config)
        take_snapshot(vm_uuid, base_snapshot_name, False, True)

    for snapshot in snapshots:
        restore_snapshot(vm_uuid, base_snapshot_name)

        # Run snapshot configured command
        cmd = snapshot.get("cmd", None)
        if cmd:
            run_command(vm_uuid, cmd)

        set_network_to_hostonly(vm_uuid)

        # Set snapshot configured legal notice
        notice_file_name = snapshot.get("legal_notice", None)
        if notice_file_name:
            notice_file_path = rf"C:\Users\{GUEST_USERNAME}\Desktop\{notice_file_name}"
            set_notice_cmd = (
                f"Import-Module $env:VM_COMMON_DIR\\vm.common\\vm.common.psm1; "
                f"VM-Set-Legal-Notice (Get-Content '{notice_file_path}' -Raw)"
            )
            run_command(vm_uuid, set_notice_cmd)

        # Perform clean up: run 'VM-Clean-Up' excluding configured files and folders
        ps_cleanup_cmd = "Import-Module $env:VM_COMMON_DIR\\vm.common\\vm.common.psm1; VM-Clean-Up"
        protected_files = snapshot.get("protected_files", None)
        if protected_files:
            ps_cleanup_cmd += f" -excludeFiles {protected_files}"
        protected_folders = snapshot.get("protected_folders", None)
        if protected_folders:
            ps_cleanup_cmd += f" -excludeFolders {protected_folders}"
        run_command(vm_uuid, ps_cleanup_cmd)

        # Perform clean up: delete PowerShells logs (using cmd.exe)
        run_command(vm_uuid, CMD_CLEANUP_CMD, "CMD")

        # Take snapshot turning the VM off
        extension = snapshot.get("extension", "")
        snapshot_name = f"{exported_vm_name}.{date}{extension}"
        take_snapshot(vm_uuid, snapshot_name, True, True)

        # Export the snapshot with the configured description
        export_vm(vm_uuid, snapshot_name, snapshot.get("description", ""))


def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("config_path", help="path of the YAML configuration file.")
    parser.add_argument(
        "--date",
        help="Date to include in the snapshots and the exported VMs in YYYYMMDD format. Today's date by default.",
        default=datetime.today().strftime("%Y%m%d"),
    )
    parser.add_argument(
        "--custom_config",
        action="store_true",
        default=False,
        help=f"flag to use a custom configuration file named 'config.xml' (expected to be in {REQUIRED_FILES_DIR}) for the FLARE-VM installation.",
    )
    parser.add_argument(
        "--do-not-install-flare-vm",
        action="store_true",
        default=False,
        help="flag to not install FLARE-VM and used an existent base snapshot. It also does not copy the required files.",
    )
    args = parser.parse_args(args=argv)

    try:
        with open(args.config_path) as f:
            config = yaml.safe_load(f)
    except Exception as e:
        print(f'Invalid "{args.config_path}": {e}')
        exit()

    build_vm(
        config["VM_NAME"],
        config["EXPORTED_VM_NAME"],
        config["SNAPSHOTS"],
        args.date,
        args.custom_config,
        args.do_not_install_flare_vm,
    )


if __name__ == "__main__":
    main()


================================================
FILE: virtualbox/vbox-build-remnux.py
================================================
#!/usr/bin/python3
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import os
import sys
from datetime import datetime

import yaml
from vboxcommon import (
    control_guest,
    ensure_vm_running,
    export_vm,
    get_vm_uuid,
    restore_snapshot,
    set_network_to_hostonly,
    take_snapshot,
)

DESCRIPTION = """
Automates the creation and export of customized REMnux virtual machines (VMs).
Begins by restoring a pre-existing "BUILD-READY" snapshot of a clean REMnux OVA.
Required installation files (such as the IDA Pro installer and ZIPs with GNOME extensions) are then copied into the guest VM.
The configuration file specifies the VM name, the exported VM name, and details for each snapshot.
Individual snapshot configurations include the extension, description, and custom commands to be executed within the guest.
"""

EPILOG = """
Example usage:
  #./vbox-build-remnux.py configs/remnux.yaml --date='19930906'
"""

BASE_SNAPSHOT = "BUILD-READY"

# Guest username and password, needed to execute commands in the guest
GUEST_USERNAME = "remnux"
GUEST_PASSWORD = "malware"

# Required files
REQUIRED_FILES_DIR = os.path.expanduser("~/REMNUX REQUIRED FILES")
REQUIRED_FILES_DEST = rf"/home/{GUEST_USERNAME}/Desktop"


def run_command(vm_uuid, cmd):
    """Run a command in the guest of the specified VM, displaying the output in real time to the console.

    Args:
        vm_uuid: VM UUID
        cmd: The command string to execute in the guest using `/bin/sh -c`.
    """
    ensure_vm_running(vm_uuid)

    executable = "/bin/sh"
    print(f"VM {vm_uuid} 🚧 {executable}: {cmd}")
    control_guest(vm_uuid, GUEST_USERNAME, GUEST_PASSWORD, ["run", executable, "--", "-c", cmd], True)


def build_vm(vm_name, exported_vm_name, snapshot, cmds, date, do_not_upgrade):
    """
    Build a REMnux VM and export it as OVA.

    Build a REMnux VM by restoring the BASE_SNAPSHOT, upgrading the REMnux distro,
    copying required files, running given commands, removing copied required files.
    Take several snapshots that can be used for debugging issues.
    Set the network to hostonly and export the resulting VM as OVA.

    Args:
        vm_name: The name of the VM.
        exported_vm_name: The base name to use for the final exported VM and snapshots.
        snapshot: A dictionary containing information about the final snapshot,
                  including optional `extension` and `description`.
        cmds: A list of string commands to execute sequentially within the guest VM.
              A snapshot is taken after executing each command.
        date: A date string to incorporate into snapshot names and the exported OVA.
        do_not_upgrade: If True, the initial upgrade step is skipped and an existent UPGRADED snapshot used.
                        It also does not copy the required files.
    """
    vm_uuid = get_vm_uuid(vm_name)
    if not vm_uuid:
        print(f'❌ ERROR: "{vm_name}" not found')
        exit()

    print(f'\nGetting the installation VM "{vm_name}" {vm_uuid} ready...')

    base_snapshot_name = f"UPGRADED.{date}"

    if not do_not_upgrade:
        restore_snapshot(vm_uuid, BASE_SNAPSHOT)

        # Copy required files
        control_guest(
            vm_uuid,
            GUEST_USERNAME,
            GUEST_PASSWORD,
            ["copyto", "--recursive", f"--target-directory={REQUIRED_FILES_DEST}", REQUIRED_FILES_DIR],
        )
        print(f"VM {vm_uuid} 📁 Copied required files in: {REQUIRED_FILES_DIR}")

        # Update REMnux distro and take a snapshot
        run_command(vm_uuid, "sudo remnux upgrade")
        take_snapshot(vm_uuid, base_snapshot_name)
    else:
        restore_snapshot(vm_uuid, base_snapshot_name)

    # Run snapshot configured commands taking a snapshot after running every command
    for i, cmd in enumerate(cmds):
        run_command(vm_uuid, cmd)
        take_snapshot(vm_uuid, f"{exported_vm_name}.{date} CMD {cmd.splitlines()[0]}")

    # Delete required files copied to the VM
    files = f"{REQUIRED_FILES_DEST}/*"
    # Sync is needed ti ensure the files deletion is written to persistent storage as the script shut down the VM abruptly
    run_command(vm_uuid, f"ls {files}; rm {files}; sync")

    set_network_to_hostonly(vm_uuid)

    # Take snapshot turning the VM off
    extension = snapshot.get("extension", "")
    snapshot_name = f"{exported_vm_name}.{date}{extension}"
    take_snapshot(vm_uuid, snapshot_name, True)

    # Export the snapshot with the configured description
    export_vm(vm_uuid, snapshot_name, snapshot.get("description", ""))


def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("config_path", help="path of the YAML configuration file.")
    parser.add_argument(
        "--date",
        help="Date to include in the snapshots and the exported VMs in YYYYMMDD format. Today's date by default.",
        default=datetime.today().strftime("%Y%m%d"),
    )
    parser.add_argument(
        "--do-not-upgrade",
        action="store_true",
        default=False,
        help="flag to not upgrade the REMnux distro and use an existent UPGRADED snapshot. It also does not copy the required files.",
    )
    args = parser.parse_args(args=argv)

    try:
        with open(args.config_path) as f:
            config = yaml.safe_load(f)
    except Exception as e:
        print(f'Invalid "{args.config_path}": {e}')
        exit()

    build_vm(
        config["VM_NAME"],
        config["EXPORTED_VM_NAME"],
        config["SNAPSHOT"],
        config["CMDS"],
        args.date,
        args.do_not_upgrade,
    )


if __name__ == "__main__":
    main()


================================================
FILE: virtualbox/vbox-clean-snapshots.py
================================================
#!/usr/bin/python3
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import argparse
import re
import sys
import textwrap

from vboxcommon import get_vm_state, run_vboxmanage

DESCRIPTION = "Clean a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name."

EPILOG = textwrap.dedent(
    """
    Example usage:
      # Delete all snapshots excluding the default protected ones (with 'clean' or 'done' in the name, case insensitive) in the 'FLARE-VM.20240604' VM
      vbox-clean-snapshots.py FLARE-VM.20240604

      # Delete all snapshots that do not include 'clean', 'done', or 'important' (case insensitive) in the name in the 'FLARE-VM.20240604' VM
      vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots "clean,done,important"

      # Delete the 'Snapshot 3' snapshot and its children recursively skipping the default protected ones in the 'FLARE-VM.20240604' VM
      vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot "Snapshot 3"

      # Delete the 'CLEAN with IDA 8.4"' children snapshots recursively skipping the default protected ones in the 'FLARE-VM.20240604' VM
      # NOTE: the 'CLEAN with IDA 8.4' root snapshot is skipped in this case
      vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot "CLEAN with IDA 8.4"

      # Delete all snapshots in the 'FLARE-VM.20240604' VM
      vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots ""
    """
)


def is_protected(protected_snapshots, snapshot_name):
    """Check if snapshot_name contains any of the strings in the protected_snapshots list (case insensitive)"""
    return any(p.lower() in snapshot_name.lower() for p in protected_snapshots)


def get_snapshot_children(vm_name, root_snapshot_name, protected_snapshots):
    """Get the children of a snapshot (including the snapshot) using 'VBoxManage snapshot' with the 'list' option.

    Args:
      vm_name: The name of the VM.
      root_snapshot_name: The name of the root snapshot we want the children of. If no provided or not found, return all snapshots.
      protected_snapshots: Snapshots we ignore and do not include in the returned list.

    Returns:
      A list of snapshot names that are children of the given snapshot. The list is ordered by dependent relationships.
    """
    # Example of `VBoxManage snapshot VM_NAME list --machinereadable` output:
    # SnapshotName="ROOT"
    # SnapshotUUID="86b38fc9-9d68-4e4b-a033-4075002ab570"
    # SnapshotName-1="Snapshot 1"
    # SnapshotUUID-1="e383e702-fee3-4e0b-b1e0-f3b869dbcaea"
    # CurrentSnapshotName="Snapshot 1"
    # CurrentSnapshotUUID="e383e702-fee3-4e0b-b1e0-f3b869dbcaea"
    # CurrentSnapshotNode="SnapshotName-1"
    # SnapshotName-1-1="Snapshot 2"
    # SnapshotUUID-1-1="8cc12787-99df-466e-8a51-80e373d3447a"
    # SnapshotName-2="Snapshot 3"
    # SnapshotUUID-2="f42533a8-7c14-4855-aa66-7169fe8187fe"
    #
    # ROOT
    #   ├─ Snapshot 1
    #   │   └─ Snapshot 2
    #   └─ Snapshot 3
    snapshots_info = run_vboxmanage(["snapshot", vm_name, "list", "--machinereadable"])

    root_snapshot_index = ""
    if root_snapshot_name:
        # Find root snapshot: first snapshot with name root_snapshot_name (case sensitive)
        root_snapshot_regex = rf'^SnapshotName(?P<index>(?:-\d+)*)="{root_snapshot_name}"\n'
        root_snapshot = re.search(root_snapshot_regex, snapshots_info, flags=re.M)
        if root_snapshot:
            root_snapshot_index = root_snapshot["index"]
        else:
            print(f"\n⚠️  Root snapshot not found: {root_snapshot_name} 🫧 Cleaning all snapshots in the VM")

    # Find all root and child snapshots as (snapshot_name, snapshot_id)
    # Children of a snapshot share the same prefix index
    index_regex = rf"{root_snapshot_index}(?:-\d+)*"
    snapshot_regex = f'^SnapshotName{index_regex}="(.*?)"\nSnapshotUUID{index_regex}="(.*?)"'
    snapshots = re.findall(snapshot_regex, snapshots_info, flags=re.M)

    # Return non protected snapshots as list of (snapshot_name, snapshot_id)
    return [snapshot for snapshot in snapshots if not is_protected(protected_snapshots, snapshot[0])]


def delete_snapshot_and_children(vm_name, snapshot_name, protected_snapshots):
    snaps_to_delete = get_snapshot_children(vm_name, snapshot_name, protected_snapshots)

    if protected_snapshots:
        print("\nSnapshots with the following strings in the name (case insensitive) won't be deleted:")
        for protected_snapshot in protected_snapshots:
            print(f"  {protected_snapshot}")

    if snaps_to_delete:
        print(f"\nCleaning {vm_name} 🫧 Snapshots to delete:")
        for snapshot_name, _ in snaps_to_delete:
            print(f"  {snapshot_name}")

        vm_state = get_vm_state(vm_name)
        if vm_state not in ("poweroff", "saved"):
            print(
                f"\nVM state: {vm_state}\n⚠️  Snapshot deleting is slower in a running VM and may fail in a changing state"
            )

        answer = input("\nConfirm deletion (press 'y'): ")
        if answer.lower() == "y":
            print("\nDELETING SNAPSHOTS... (this may take some time, go for an 🍦!)")
            # Delete snapshots in reverse order to avoid issues with child snapshots,
            # as a snapshot with more than 1 child can not be deleted
            for snapshot_name, snapshot_id in reversed(snaps_to_delete):
                try:
                    run_vboxmanage(["snapshot", vm_name, "delete", snapshot_id])
                    print(f"🫧 DELETED '{snapshot_name}'")
                except Exception as e:
                    print(f"❌ ERROR '{snapshot_name}'\n{e}")
    else:
        print(f"\n{vm_name} is clean 🫧")

    print("\nSee you next time you need to clean up your VMs! ✨\n")


def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    epilog = EPILOG
    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=epilog,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("vm_name", help="Name of the VM to clean up")
    parser.add_argument(
        "--root_snapshot",
        help="""Snapshot name (case sensitive) to delete (and its children recursively).
                Leave empty to clean all snapshots in the VM.""",
    )
    parser.add_argument(
        "--protected_snapshots",
        default="clean,done",
        type=lambda s: s.split(",") if s else [],
        help='''Comma-separated list of strings.
                Snapshots with any of the strings included in the name (case insensitive) are not deleted.
                Default: "clean,done"''',
    )
    args = parser.parse_args(args=argv)

    delete_snapshot_and_children(args.vm_name, args.root_snapshot, args.protected_snapshots)


if __name__ == "__main__":
    main()


================================================
FILE: virtualbox/vbox-export-snapshot.py
================================================
#!/usr/bin/python3
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import sys

from vboxcommon import (
    EXPORT_DIR_NAME,
    LONG_WAIT,
    ensure_vm_running,
    export_vm,
    get_vm_uuid,
    restore_snapshot,
    set_network_to_hostonly,
)

DESCRIPTION = """Export a snapshot to OVA (named after the snapshot) with a single Host-Only network interface.
Generate a file containing the SHA256 hash of the OVA that can be used for verification."""

EPILOG = """
Example usage:
  # Export snapshot "FLARE-VM" from the "FLARE-VM.testing" VM with a description
  ./vbox-export-snapshot.py "FLARE-VM.testing" "FLARE-VM" --description "Windows 10 VM with FLARE-VM default configuration"
"""


def export_snapshot(vm_name, snapshot, description, export_dir_name):
    """Restore a snapshot, set the network to hostonly and then export it with the snapshot as name."""
    vm_uuid = get_vm_uuid(vm_name)
    if not vm_uuid:
        print(f'❌ ERROR: "{vm_name}" not found')
        exit()

    print(f'\nExporting snapshot "{snapshot}" from "{vm_name}" {vm_uuid}...')
    try:
        restore_snapshot(vm_uuid, snapshot)

        set_network_to_hostonly(vm_uuid)

        # Start the VM to ensure everything is good
        print(f"VM {vm_uuid} 🔄 power cycling before export{LONG_WAIT}")
        ensure_vm_running(vm_uuid)
        export_vm(vm_uuid, snapshot, description, export_dir_name)
    except Exception as e:
        print(f'VM {vm_uuid} ❌ ERROR exporting "{snapshot}": {e}\n')


def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("vm_name", help="name of the VM to export a snapshot from.")
    parser.add_argument("snapshot", help="name of the snapshot to export.")
    parser.add_argument("--description", help="description of the exported OVA. Empty by default.")
    parser.add_argument(
        "--export_dir_name",
        help=f"name of the directory in HOME to export the VMs The directory is created if it does not exist. Default: {EXPORT_DIR_NAME}",
    )
    args = parser.parse_args(args=argv)

    export_snapshot(args.vm_name, args.snapshot, args.description, args.export_dir_name)


if __name__ == "__main__":
    main()


================================================
FILE: virtualbox/vboxcommon.py
================================================
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import hashlib
import os
import re
import subprocess
import sys
import time
from datetime import datetime

# Message to add to the output when waiting for a long operation to complete.
LONG_WAIT = "... (it will take some time, go for an 🍦!)"

# Default name of the directory in HOME to export VMs to
EXPORT_DIR_NAME = "EXPORTED VMS"


def format_arg(arg):
    """Add quotes to the string arg if it contains special characters like spaces."""
    if any(c in arg for c in (" ", "\\", "/")):
        if "'" not in arg:
            return f"'{arg}'"
        if '"' not in arg:
            return f'"{arg}"'
    return arg


def cmd_to_str(cmd):
    """Convert a list of string arguments to a string."""
    return " ".join(format_arg(arg) for arg in cmd)


def __run_vboxmanage(cmd, real_time=False):
    """Run a command using 'subprocess.run' and return the output.

    Args:
        cmd: list with the command and its arguments
        real_time: Boolean that determines if displaying the output in realtime or returning it.
    """
    # When running as a PyInstaller bundle, LD_LIBRARY_PATH is set,
    # which can cause conflicts with external binaries like VBoxManage.
    # We create a clean environment for the subprocess to use the system's libraries.
    env = os.environ.copy()
    if getattr(sys, "frozen", False) and "LD_LIBRARY_PATH" in env:
        # 'sys.frozen' is True when running from a PyInstaller executable.
        # We can either remove the variable or, more safely, restore the original
        # one if PyInstaller saved it. PyInstaller often saves it as LD_LIBRARY_PATH_ORIG.
        if "LD_LIBRARY_PATH_ORIG" in env:
            env["LD_LIBRARY_PATH"] = env["LD_LIBRARY_PATH_ORIG"]
        else:
            del env["LD_LIBRARY_PATH"]

    if real_time:
        return subprocess.run(cmd, stderr=sys.stderr, stdout=sys.stdout, env=env)
    else:
        return subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)


def run_vboxmanage(cmd, real_time=False):
    """Run a VBoxManage command and return the output.

    Args:
        cmd: list of string arguments to pass to VBoxManage
        real_time: Boolean that determines if displaying the output in realtime or returning it.
    """
    cmd = ["VBoxManage"] + cmd
    result = __run_vboxmanage(cmd, real_time)

    if result.returncode:
        # Check if we are affect by the following VERR_NO_LOW_MEMORY bug: https://www.virtualbox.org/ticket/22185
        # and re-run the command every minute until the VERR_NO_LOW_MEMORY error is resolved
        while result.stdout and "VERR_NO_LOW_MEMORY" in result.stdout:
            print("❌ VirtualBox VERR_NO_LOW_MEMORY error (likely https://www.virtualbox.org/ticket/22185)")
            print("🩹 Fit it running 'echo 3 | sudo tee /proc/sys/vm/drop_caches'")
            print("⏳ I'll re-try the command in ~ 1 minute\n")
            time.sleep(60)  # wait 1 minutes

            # Re-try command
            result = __run_vboxmanage(cmd, real_time)

    if result.returncode:
        error = f"Command '{cmd_to_str(cmd)}' failed"
        # Use only the first "VBoxManage: error:" line to prevent using the long
        # VBoxManage help message or noisy information like the details and context.
        if result.stdout:
            match = re.search("^VBoxManage: error: (?P<err_info>.*)", result.stdout, flags=re.M)
            if match:
                error += f": {match['err_info']}"
        raise RuntimeError(error)

    return result.stdout


def control_guest(vm_uuid, user, password, args, real_time=False):
    """Run a 'VBoxManage guestcontrol' command providing the username and password.
    Args:
        vm_uuid: VM UUID
        args: list of arguments starting with the guestcontrol sub-command
        real_time: Boolean that determines if displaying the output in realtime or returning it.
    """
    # VM must be running to control the guest
    ensure_vm_running(vm_uuid)
    cmd = ["guestcontrol", vm_uuid, f"--username={user}", f"--password={password}"] + args
    try:
        return run_vboxmanage(cmd, real_time)
    except RuntimeError:
        # The guest additions take a bit to load after the user is logged in
        # In slow environments this may cause the command to fail, wait a bit and re-try
        time.sleep(120)  # Wait 2 minutes
        return run_vboxmanage(cmd, real_time)


def get_hostonlyif_name():
    """Get the name of the host-only interface. Return None if there is no host-only interface"""
    # Example of `VBoxManage list hostonlyifs` relevant output:
    # Name:            vboxnet0
    hostonlyifs_info = run_vboxmanage(["list", "hostonlyifs"])

    match = re.search(r"^Name: *(?P<hostonlyif_name>\S+)", hostonlyifs_info, flags=re.M)
    if match:
        return match["hostonlyif_name"]


def ensure_hostonlyif_exists():
    """Get the name of the host-only interface. Create the interface if it doesn't exist."""
    hostonlyif_name = get_hostonlyif_name()

    if not hostonlyif_name:
        # No host-only interface found, create one
        run_vboxmanage(["hostonlyif", "create"])

        hostonlyif_name = get_hostonlyif_name()
        if not hostonlyif_name:
            raise RuntimeError("Failed to create new hostonly interface.")

        print(f"Hostonly interface created: {hostonlyif_name}")

    return hostonlyif_name


def set_network_to_hostonly(vm_uuid):
    """Set the NIC 1 to hostonly and disable the rest."""
    # VM must be shutdown before changing the adapters
    ensure_vm_shutdown(vm_uuid)

    # Ensure a hostonly interface exists to prevent issues starting the VM
    ensure_hostonlyif_exists()

    # Example of `VBoxManage showvminfo <VM_UUID> --machinereadable` relevant output:
    # nic1="none"
    # bridgeadapter2="wlp9s0"
    # macaddress2="0800271DDA9D"
    # cableconnected2="on"
    # nic2="bridged"
    # nictype2="82540EM"
    # nicspeed2="0"
    # nic3="none"
    # nic4="none"
    # nic5="none"
    # nic6="none"
    # nic7="none"
    # nic8="none"
    vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])

    # Set all NICs to none to avoid running into strange situations
    for nic_number, nic_value in re.findall(r'^nic(\d+)="(\S+)"', vm_info, flags=re.M):
        if nic_value != "none":  # Ignore NICs that are already none
            run_vboxmanage(["modifyvm", vm_uuid, f"--nic{nic_number}", "none"])

    # Set NIC 1 to hostonly
    run_vboxmanage(["modifyvm", vm_uuid, "--nic1", "hostonly"])

    # Ensure changes applied
    vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])
    nic_values = re.findall(r'^nic\d+="(\S+)"', vm_info, flags=re.M)
    if nic_values[0] != "hostonly" or any(nic_value != "none" for nic_value in nic_values[1:]):
        raise RuntimeError(f"Unable to change NICs to a single hostonly in VM {vm_uuid}")

    print(f"VM {vm_uuid} ⚙️  network set to single hostonly adapter")


def sha256_file(filepath):
    """Return the SHA256 of the content of the file provided as argument."""
    with open(filepath, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


def export_vm(vm_uuid, exported_vm_name, description="", export_dir_name=EXPORT_DIR_NAME):
    """Export VM as OVA and generate a file with the SHA256 of the exported OVA."""
    # Create export directory
    export_directory = os.path.expanduser(f"~/{export_dir_name}")
    os.makedirs(export_directory, exist_ok=True)

    exported_ova_filepath = os.path.join(export_directory, f"{exported_vm_name}.ova")

    # Rename OVA if it already exists (for example if the script is called twice) or exporting will fail
    if os.path.exists(exported_ova_filepath):
        time_str = datetime.now().strftime("%H_%M")
        old_ova_filepath = os.path.join(export_directory, f"{exported_vm_name}.{time_str}.ova")
        os.rename(exported_ova_filepath, old_ova_filepath)
        print(f"⚠️  Renamed old OVA to export new one: {old_ova_filepath}")

    # Turn off VM and export it to .ova
    ensure_vm_shutdown(vm_uuid)
    print(f"VM {vm_uuid} 🚧 exporting {LONG_WAIT}")
    run_vboxmanage(
        [
            "export",
            vm_uuid,
            f"--output={exported_ova_filepath}",
            "--vsys=0",  # We need to specify the index of the VM, 0 as we only export 1 VM
            f"--vmname={exported_vm_name}",
            f"--description={description}",
        ]
    )
    print(f'VM {vm_uuid} ✅ EXPORTED "{exported_ova_filepath}"')

    # Generate file with SHA256
    sha256 = sha256_file(exported_ova_filepath)
    sha256_filepath = f"{exported_ova_filepath}.sha256"
    with open(sha256_filepath, "w") as f:
        f.write(sha256)

    print(f'VM {vm_uuid} ✅ GENERATED "{sha256_filepath}": {sha256}\n')


def get_vm_uuid(vm_name):
    """Get the machine UUID for a given VM name using 'VBoxManage list vms'. Return None if not found."""
    # regex VM name and extract the GUID
    # Example of `VBoxManage list vms` output:
    # "FLARE-VM.testing" {b76d628b-737f-40a3-9a16-c5f66ad2cfcc}
    # "FLARE-VM" {a23c0c37-2062-4cf0-882b-9e9747dd33b6}
    vms_info = run_vboxmanage(["list", "vms"])

    match = re.search(rf'^"{vm_name}" (?P<uuid>\{{.*?\}})', vms_info, flags=re.M)
    if match:
        return match.group("uuid")


def get_vm_state(vm_uuid):
    """Get the VM state using 'VBoxManage showvminfo'."""
    # Example of `VBoxManage showvminfo <VM_UUID> --machinereadable` relevant output:
    # VMState="poweroff"
    vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"])

    match = re.search(r'^VMState="(?P<state>\S+)"', vm_info, flags=re.M)
    if match:
        return match["state"]

    raise Exception(f"Unable to get state of VM {vm_uuid}")


def get_num_logged_in_users(vm_uuid):
    """Return the number of logged in users using 'VBoxManage guestproperty'."""
    # Examples of 'VBoxManage guestproperty get <VM_UUID> "/VirtualBox/GuestInfo/OS/LoggedInUsers"' output:
    # - 'Value: 1'
    # - 'Value: 0'
    # - 'No value set!'
    logged_in_users_info = run_vboxmanage(["guestproperty", "get", vm_uuid, "/VirtualBox/GuestInfo/OS/LoggedInUsers"])

    if logged_in_users_info:
        match = re.search(r"^Value: (?P<logged_in_users>\d+)", logged_in_users_info)
        if match:
            return int(match["logged_in_users"])
    return 0


def wait_until(vm_uuid, condition):
    """Wait for VM to verify a condition

    Return True if the condition is met within one minute.
    Return False otherwise.
    """
    timeout = 600  # seconds (10 minutes)
    check_interval = 5  # seconds
    star
Download .txt
gitextract_n6_o4m_t/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug.yml
│   │   ├── config.yml
│   │   └── feature.yml
│   └── workflows/
│       ├── build-vbox.yaml
│       └── linter.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE.txt
├── LayoutModification.xml
├── README.md
├── config.xml
├── install.ps1
├── scripts/
│   └── lint.ps1
└── virtualbox/
    ├── README.md
    ├── configs/
    │   ├── remnux.yaml
    │   ├── win10_flare-vm-edu.yaml
    │   ├── win10_flare-vm.yaml
    │   └── win11_flare-vm.yaml
    ├── install.sh
    ├── vbox-adapter-check.py
    ├── vbox-build-flare-vm.py
    ├── vbox-build-remnux.py
    ├── vbox-clean-snapshots.py
    ├── vbox-export-snapshot.py
    └── vboxcommon.py
Download .txt
SYMBOL INDEX (39 symbols across 6 files)

FILE: virtualbox/vbox-adapter-check.py
  function get_vms (line 58) | def get_vms(dynamic_only):
  function get_nics (line 83) | def get_nics(vm_uuid, only_nic=None):
  function disable_adapter (line 119) | def disable_adapter(vm_uuid, nic_number, hostonly_ifname):
  function list_to_str (line 166) | def list_to_str(string_list):
  function verify_network_adapters (line 171) | def verify_network_adapters(vm_uuid, vm_name, hostonly_ifname, modify_an...
  function main (line 222) | def main(argv=None):

FILE: virtualbox/vbox-build-flare-vm.py
  function run_command (line 82) | def run_command(vm_uuid, cmd, executable="PS"):
  function create_log_folder (line 98) | def create_log_folder():
  function install_flare_vm (line 110) | def install_flare_vm(vm_uuid, snapshot_name, custom_config):
  function build_vm (line 167) | def build_vm(vm_name, exported_vm_name, snapshots, date, custom_config, ...
  function main (line 260) | def main(argv=None):

FILE: virtualbox/vbox-build-remnux.py
  function run_command (line 56) | def run_command(vm_uuid, cmd):
  function build_vm (line 70) | def build_vm(vm_name, exported_vm_name, snapshot, cmds, date, do_not_upg...
  function main (line 138) | def main(argv=None):

FILE: virtualbox/vbox-clean-snapshots.py
  function is_protected (line 48) | def is_protected(protected_snapshots, snapshot_name):
  function get_snapshot_children (line 53) | def get_snapshot_children(vm_name, root_snapshot_name, protected_snapsho...
  function delete_snapshot_and_children (line 103) | def delete_snapshot_and_children(vm_name, snapshot_name, protected_snaps...
  function main (line 139) | def main(argv=None):

FILE: virtualbox/vbox-export-snapshot.py
  function export_snapshot (line 39) | def export_snapshot(vm_name, snapshot, description, export_dir_name):
  function main (line 60) | def main(argv=None):

FILE: virtualbox/vboxcommon.py
  function format_arg (line 30) | def format_arg(arg):
  function cmd_to_str (line 40) | def cmd_to_str(cmd):
  function __run_vboxmanage (line 45) | def __run_vboxmanage(cmd, real_time=False):
  function run_vboxmanage (line 71) | def run_vboxmanage(cmd, real_time=False):
  function control_guest (line 106) | def control_guest(vm_uuid, user, password, args, real_time=False):
  function get_hostonlyif_name (line 125) | def get_hostonlyif_name():
  function ensure_hostonlyif_exists (line 136) | def ensure_hostonlyif_exists():
  function set_network_to_hostonly (line 153) | def set_network_to_hostonly(vm_uuid):
  function sha256_file (line 194) | def sha256_file(filepath):
  function export_vm (line 200) | def export_vm(vm_uuid, exported_vm_name, description="", export_dir_name...
  function get_vm_uuid (line 239) | def get_vm_uuid(vm_name):
  function get_vm_state (line 252) | def get_vm_state(vm_uuid):
  function get_num_logged_in_users (line 265) | def get_num_logged_in_users(vm_uuid):
  function wait_until (line 280) | def wait_until(vm_uuid, condition):
  function ensure_vm_running (line 297) | def ensure_vm_running(vm_uuid):
  function ensure_vm_shutdown (line 309) | def ensure_vm_shutdown(vm_uuid):
  function restore_snapshot (line 332) | def restore_snapshot(vm_uuid, snapshot_name):
  function rename_old_snapshot (line 341) | def rename_old_snapshot(vm_uuid, snapshot_name):
  function take_snapshot (line 361) | def take_snapshot(vm_uuid, snapshot_name, shutdown=False, rename=False):
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (214K chars).
[
  {
    "path": ".gitattributes",
    "chars": 550,
    "preview": "# Set default behaviour, in case users don't have core.autocrlf set.\n* text=auto\n\n# Explicitly declare text files we wan"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "chars": 3212,
    "preview": "name: 🐛 Bug\ndescription: You need help installing FLARE-VM or something doesn't work as expected\nlabels: [\":bug: bug\"]\nb"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 28,
    "preview": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "chars": 1109,
    "preview": "name: 💡 Feature proposal\ndescription: Propose a new feature or improvement.\nbody:\n  - type: markdown\n    attributes:\n   "
  },
  {
    "path": ".github/workflows/build-vbox.yaml",
    "chars": 2793,
    "preview": "name: Build & release vbox\n# Create or update a release by adding a body and the built vbox binaries on a tag creation t"
  },
  {
    "path": ".github/workflows/linter.yml",
    "chars": 824,
    "preview": "name: Linter\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  lint:\n    runs-on: wind"
  },
  {
    "path": ".gitignore",
    "chars": 521,
    "preview": "# Compiled source #\n###################\n*.com\n*.class\n*.o\n*.so\n__pycache__/\n\n# Packages #\n############\n# it's better to "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2058,
    "preview": "# How to contribute\n\nWant to open an issue or send a code contribution?\nRead the information below to learn how.\nWe are "
  },
  {
    "path": "LICENSE.txt",
    "chars": 11560,
    "preview": "\r\n                                 Apache License\r\n                           Version 2.0, January 2004\r\n               "
  },
  {
    "path": "LayoutModification.xml",
    "chars": 2623,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<!--\r\n Copyright 2023 Google LLC\r\n\r\n Licensed under the Apache License, Version "
  },
  {
    "path": "README.md",
    "chars": 11194,
    "preview": "# FLARE-VM\nWelcome to FLARE-VM - a collection of software installations scripts for Windows systems that allows you to e"
  },
  {
    "path": "config.xml",
    "chars": 11002,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<!--\r\n Copyright 2017 Google LLC\r\n\r\n Licensed under the Apache License, Version "
  },
  {
    "path": "install.ps1",
    "chars": 83144,
    "preview": "<#\r\n Copyright 2017 Google LLC\r\n\r\n Licensed under the Apache License, Version 2.0 (the \"License\");\r\n you may not use thi"
  },
  {
    "path": "scripts/lint.ps1",
    "chars": 868,
    "preview": "# Exclude rules that make the code less readable or involve changing the functionality\r\n$excludedRules = \"PSAvoidUsingPl"
  },
  {
    "path": "virtualbox/README.md",
    "chars": 7086,
    "preview": "# VirtualBox scripts\n\n**This folder contains several scripts related to enhance building, exporting, and using FLARE-VM "
  },
  {
    "path": "virtualbox/configs/remnux.yaml",
    "chars": 2489,
    "preview": "VM_NAME: REMnux.testing\nEXPORTED_VM_NAME: REMnux\nSNAPSHOT:\n  extension: \".dynamic\"\n  description: \"REMnux (based on Ubun"
  },
  {
    "path": "virtualbox/configs/win10_flare-vm-edu.yaml",
    "chars": 983,
    "preview": "VM_NAME: FLARE-VM.testing\nEXPORTED_VM_NAME: FLARE-VM.Win10\nSNAPSHOTS:\n- extension: \".EDU\"\n  description: \"Windows 10 VM "
  },
  {
    "path": "virtualbox/configs/win10_flare-vm.yaml",
    "chars": 811,
    "preview": "VM_NAME: FLARE-VM.testing\nEXPORTED_VM_NAME: FLARE-VM.Win10\nSNAPSHOTS:\n- extension: \".dynamic\"\n  description: \"Windows 10"
  },
  {
    "path": "virtualbox/configs/win11_flare-vm.yaml",
    "chars": 817,
    "preview": "VM_NAME: FLARE-VM.Win11.testing\nEXPORTED_VM_NAME: FLARE-VM.Win11\nSNAPSHOTS:\n- extension: \".dynamic\"\n  description: \"Wind"
  },
  {
    "path": "virtualbox/install.sh",
    "chars": 2900,
    "preview": "#!/bin/bash\n\n# This script configures the vbox-adapter-check file to run automatically. It performs setup of a cron task"
  },
  {
    "path": "virtualbox/vbox-adapter-check.py",
    "chars": 9506,
    "preview": "#!/usr/bin/python3\n# Copyright 2024 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "virtualbox/vbox-build-flare-vm.py",
    "chars": 12743,
    "preview": "#!/usr/bin/python3\n# Copyright 2024 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "virtualbox/vbox-build-remnux.py",
    "chars": 6340,
    "preview": "#!/usr/bin/python3\n# Copyright 2024 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "virtualbox/vbox-clean-snapshots.py",
    "chars": 7340,
    "preview": "#!/usr/bin/python3\n# Copyright 2024 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "virtualbox/vbox-export-snapshot.py",
    "chars": 2891,
    "preview": "#!/usr/bin/python3\n# Copyright 2024 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you"
  },
  {
    "path": "virtualbox/vboxcommon.py",
    "chars": 14787,
    "preview": "# Copyright 2024 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  }
]

About this extraction

This page contains the full source code of the mandiant/flare-vm GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (195.5 KB), approximately 49.4k tokens, and a symbol index with 39 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!