diff --git a/.github/workflows/dottest_autofix.yml b/.github/workflows/dottest_autofix.yml new file mode 100644 index 0000000..61bbaa3 --- /dev/null +++ b/.github/workflows/dottest_autofix.yml @@ -0,0 +1,86 @@ +# This workflow runs Parasoft dotTEST to analyze code +# and display results with Github code scanning alerts. +# Parasoft dotTEST is a testing tool that provides code analysis techniques +# to improve code quality and ensure compliance with industry standards. +# See https://github.com/parasoft/run-dottest-action for more information. + +name: Parasoft dotTEST Code Analysis + +on: + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel. +jobs: + # This workflow is made up of one job "run-dottest-action". + run-dottest-action: + # Specifies the name of the job. + name: Run code analysis with dotTEST + + # Specifies required permissions for upload-sarif action + permissions: + # required for all workflows + security-events: write + # only required for workflows in private repositories + actions: read + contents: read + + # Specifies the type of runner that the job will run on. + runs-on: self-hosted + + # Steps represent a sequence of tasks that will be executed as part of the job. + steps: + + # Checks out your repository, so that your job can access it. + - name: Check out code + uses: actions/checkout@v4 + + # --------------------------------------------------------------- + # Runs code analysis with dotTEST and generates a .sarif report. + - name: Run Parasoft dotTEST + id: dottest + uses: parasoft/run-dottest-action@2.0.2 + with: + # Path to the dotTEST installation directory, which contains dottestcli.exe. If not specified, dottestcli.exe will be searched for on PATH. + #installDir: # optional + # Path to the project to be analyzed when no solution is provided. Specify a semicolon-separated list of paths to analyze many projects. Supports ANT-style wildcards. + testConfig: Recommended Rules + #settings: # optional + # A single configuration setting in the "key=value" format. + #property: # optional + # Solution configuration, e.g. "Debug". + #solutionConfig: Debug + # Target platform of the solution configuration (e.g."Any CPU") or project configuration (e.g. "AnyCPU"). + #targetPlatform: "Any CPU" + # Path to the location where console output is saved. + #out: ${{ github.workspace }}/.dottest/report/${{ github.run_number }}/output.txt + + # --------------------------------------------------------------- + # Uploads an archive that includes all report files (.xml, .html, .sarif). + - name: Upload report artifacts + uses: actions/upload-artifact@v3 + if: always() + with: + name: Report files + path: ${{ steps.dottest.outputs.reportDir }}/*.* + + # --------------------------------------------------------------- + # Uploads analysis results in the SARIF format, so that they are displayed as GitHub code scanning alerts. + - name: Upload results to GitHub + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.dottest.outputs.report }} + + + # --------------------------------------------------------------- + # Runs code autofix + - name: Run code autofix + if: always() + shell: bash + run: | + python "${{ steps.dottest.outputs.installDir }}/integration/aider/DottestAutoFix.py" \ + --report "${{ steps.dottest.outputs.report }}" \ + --tool-home "${{ steps.dottest.outputs.installDir }}" \ + --solution "${{ steps.dottest.outputs.solution }}" \ + --fix-limit 3 diff --git a/WebGoat.NET/Controllers/AccountController.cs b/WebGoat.NET/Controllers/AccountController.cs index d2309a7..47d964c 100644 --- a/WebGoat.NET/Controllers/AccountController.cs +++ b/WebGoat.NET/Controllers/AccountController.cs @@ -1,252 +1,253 @@ -using WebGoatCore.Data; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; -using WebGoatCore.ViewModels; -using Microsoft.AspNetCore.Authorization; +using WebGoatCore.Data; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using WebGoatCore.ViewModels; +using Microsoft.AspNetCore.Authorization; using WebGoatCore.Models; -namespace WebGoatCore.Controllers -{ - [Authorize] - public class AccountController : Controller - { - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly CustomerRepository _customerRepository; - - public AccountController(UserManager userManager, SignInManager signInManager, CustomerRepository customerRepository) - { - _userManager = userManager; - _signInManager = signInManager; - _customerRepository = customerRepository; - } - - [HttpGet] - [AllowAnonymous] - public IActionResult Login(string? returnUrl) - { - return View(new LoginViewModel - { - ReturnUrl = returnUrl - }); - } - - [HttpPost] - [AllowAnonymous] - public async Task Login(LoginViewModel model) - { - if (!ModelState.IsValid) - { - return View(model); - } - - var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, lockoutOnFailure: true); - - if (result.Succeeded) - { - if (model.ReturnUrl != null) - { - return Redirect(model.ReturnUrl); - } - else - { - return RedirectToAction("Index", "Home"); - } - } - - if (result.IsLockedOut) - { - return RedirectToPage("./Lockout"); - } - else - { - ModelState.AddModelError(string.Empty, "Invalid login attempt."); - return View(model); - } - } - - public async Task Logout() - { - await _signInManager.SignOutAsync(); - HttpContext.Session.Set("Cart", new Cart()); - return RedirectToAction("Index", "Home"); - } - - [HttpGet] - [AllowAnonymous] - public async Task Register() - { - await _signInManager.SignOutAsync(); - return View(new RegisterViewModel()); - } - - [HttpPost] - [AllowAnonymous] - public async Task Register(RegisterViewModel model) - { - if (ModelState.IsValid) - { - var user = new IdentityUser(model.Username) - { - Email = model.Email - }; - - var result = await _userManager.CreateAsync(user, model.Password); - if (result.Succeeded) - { - _customerRepository.CreateCustomer(model.CompanyName, model.Username, model.Address, model.City, model.Region, model.PostalCode, model.Country); - - await _signInManager.SignInAsync(user, isPersistent: false); - return RedirectToAction("Index", "Home"); - } - - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - } - - return View(model); - } - - public IActionResult MyAccount() => View(); - - public IActionResult ViewAccountInfo() - { - var customer = _customerRepository.GetCustomerByUsername(_userManager.GetUserName(User)); - if (customer == null) - { - ModelState.AddModelError(string.Empty, "We don't recognize your customer Id. Please log in and try again."); - } - - return View(customer); - } - - [HttpGet] - public IActionResult ChangeAccountInfo() - { - var customer = _customerRepository.GetCustomerByUsername(_userManager.GetUserName(User)); - if (customer == null) - { - ModelState.AddModelError(string.Empty, "We don't recognize your customer Id. Please log in and try again."); - return View(new ChangeAccountInfoViewModel()); - } - - return View(new ChangeAccountInfoViewModel() - { - CompanyName = customer.CompanyName, - ContactTitle = customer.ContactTitle, - Address = customer.Address, - City = customer.City, - Region = customer.Region, - PostalCode = customer.PostalCode, - Country = customer.Country, - }); - } - - [HttpPost] - public IActionResult ChangeAccountInfo(ChangeAccountInfoViewModel model) - { - var customer = _customerRepository.GetCustomerByUsername(_userManager.GetUserName(User)); - if (customer == null) - { - ModelState.AddModelError(string.Empty, "We don't recognize your customer Id. Please log in and try again."); - return View(model); - } - - if (ModelState.IsValid) - { - customer.CompanyName = model.CompanyName ?? customer.CompanyName; - customer.ContactTitle = model.ContactTitle ?? customer.ContactTitle; - customer.Address = model.Address ?? customer.Address; - customer.City = model.City ?? customer.City; - customer.Region = model.Region ?? customer.Region; - customer.PostalCode = model.PostalCode ?? customer.PostalCode; - customer.Country = model.Country ?? customer.Country; - _customerRepository.SaveCustomer(customer); - - model.UpdatedSucessfully = true; - } - - return View(model); - } - - [HttpGet] - public IActionResult ChangePassword() => View(new ChangePasswordViewModel()); - - [HttpPost] - public async Task ChangePassword(ChangePasswordViewModel model) - { - if (ModelState.IsValid) - { - var result = await _userManager.ChangePasswordAsync(await _userManager.GetUserAsync(User), model.OldPassword, model.NewPassword); - if (result.Succeeded) - { - return View("ChangePasswordSuccess"); - } - - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - } - - return View(model); - } - - [HttpGet] - public IActionResult AddUserTemp() - { - var model = new AddUserTempViewModel - { - IsIssuerAdmin = User.IsInRole("Admin"), - }; - return View(model); - } - - [HttpPost] - public async Task AddUserTemp(AddUserTempViewModel model) - { - if(!model.IsIssuerAdmin) - { - return RedirectToAction("Login"); - } - - if (ModelState.IsValid) - { - var user = new IdentityUser(model.NewUsername) - { - Email = model.NewEmail - }; - - var result = await _userManager.CreateAsync(user, model.NewPassword); - if (result.Succeeded) - { - if (model.MakeNewUserAdmin) - { - // TODO: role should be Admin? - result = await _userManager.AddToRoleAsync(user, "admin"); - if (!result.Succeeded) - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - } - } - } - else - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - } - } - - model.CreatedUser = true; - return View(model); - } - } -} \ No newline at end of file + +namespace WebGoatCore.Controllers +{ + [Authorize] + public class AccountController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly CustomerRepository _customerRepository; + + public AccountController(UserManager userManager, SignInManager signInManager, CustomerRepository customerRepository) + { + _userManager = userManager; + _signInManager = signInManager; + _customerRepository = customerRepository; + } + + [HttpGet] + [AllowAnonymous] + public IActionResult Login(string? returnUrl) + { + return View(new LoginViewModel + { + ReturnUrl = returnUrl + }); + } + + [HttpPost] + [AllowAnonymous] + public async Task Login(LoginViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, lockoutOnFailure: true); + + if (result.Succeeded) + { + if (model.ReturnUrl != null) + { + return Redirect(model.ReturnUrl); + } + else + { + return RedirectToAction("Index", "Home"); + } + } + + if (result.IsLockedOut) + { + return RedirectToPage("./Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + } + } + + public async Task Logout() + { + await _signInManager.SignOutAsync(); + HttpContext.Session.Set("Cart", new Cart()); + return RedirectToAction("Index", "Home"); + } + + [HttpGet] + [AllowAnonymous] + public async Task Register() + { + await _signInManager.SignOutAsync(); + return View(new RegisterViewModel()); + } + + [HttpPost] + [AllowAnonymous] + public async Task Register(RegisterViewModel model) + { + if (ModelState.IsValid) + { + var user = new IdentityUser(model.Username) + { + Email = model.Email + }; + + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + _customerRepository.CreateCustomer(model.CompanyName, model.Username, model.Address, model.City, model.Region, model.PostalCode, model.Country); + + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction("Index", "Home"); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + return View(model); + } + + public IActionResult MyAccount() => View(); + + public IActionResult ViewAccountInfo() + { + var customer = _customerRepository.GetCustomerByUsername(_userManager.GetUserName(User)); + if (customer == null) + { + ModelState.AddModelError(string.Empty, "We don't recognize your customer Id. Please log in and try again."); + } + + return View(customer); + } + + [HttpGet] + public IActionResult ChangeAccountInfo() + { + var customer = _customerRepository.GetCustomerByUsername(_userManager.GetUserName(User)); + if (customer == null) + { + ModelState.AddModelError(string.Empty, "We don't recognize your customer Id. Please log in and try again."); + return View(new ChangeAccountInfoViewModel()); + } + + return View(new ChangeAccountInfoViewModel() + { + CompanyName = customer.CompanyName, + ContactTitle = customer.ContactTitle, + Address = customer.Address, + City = customer.City, + Region = customer.Region, + PostalCode = customer.PostalCode, + Country = customer.Country, + }); + } + + [HttpPost] + public IActionResult ChangeAccountInfo(ChangeAccountInfoViewModel model) + { + var customer = _customerRepository.GetCustomerByUsername(_userManager.GetUserName(User)); + if (customer == null) + { + ModelState.AddModelError(string.Empty, "We don't recognize your customer Id. Please log in and try again."); + return View(model); + } + + if (ModelState.IsValid) + { + customer.CompanyName = model.CompanyName ?? customer.CompanyName; + customer.ContactTitle = model.ContactTitle ?? customer.ContactTitle; + customer.Address = model.Address ?? customer.Address; + customer.City = model.City ?? customer.City; + customer.Region = model.Region ?? customer.Region; + customer.PostalCode = model.PostalCode ?? customer.PostalCode; + customer.Country = model.Country ?? customer.Country; + _customerRepository.SaveCustomer(customer); + + model.UpdatedSucessfully = true; + } + + return View(model); + } + + [HttpGet] + public IActionResult ChangePassword() => View(new ChangePasswordViewModel()); + + [HttpPost] + public async Task ChangePassword(ChangePasswordViewModel model) + { + if (ModelState.IsValid) + { + var result = await _userManager.ChangePasswordAsync(await _userManager.GetUserAsync(User), model.OldPassword, model.NewPassword); + if (result.Succeeded) + { + return View("ChangePasswordSuccess"); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + return View(model); + } + + [HttpGet] + public IActionResult AddUserTemp() + { + var model = new AddUserTempViewModel + { + IsIssuerAdmin = User.IsInRole("Admin"), + }; + return View(model); + } + + [HttpPost] + public async Task AddUserTemp(AddUserTempViewModel model) + { + if(!model.IsIssuerAdmin) + { + return RedirectToAction("Login"); + } + + if (ModelState.IsValid) + { + var user = new IdentityUser(model.NewUsername) + { + Email = model.NewEmail + }; + + var result = await _userManager.CreateAsync(user, model.NewPassword); + if (result.Succeeded) + { + if (model.MakeNewUserAdmin) + { + // TODO: should role of the user granting admin role should be Admin as well? + result = await _userManager.AddToRoleAsync(user, "admin"); + if (!result.Succeeded) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + } + } + else + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + } + + model.CreatedUser = true; + return View(model); + } + } +} diff --git a/readme.md b/readme.md index 6cba364..74c699c 100644 --- a/readme.md +++ b/readme.md @@ -1,36 +1,78 @@ -# WebGoat.NET version 0.3 +# dotTEST WebGoat.NET Example -## Build status +This example shows the following dotTEST capabilities: + +- Static analysis and flow analysis capabilities +- Integration with GitHub pipelines via the [Run dotTEST Action](https://github.com/parasoft/run-dottest-action) +- Integration with Aider to apply static analysis fixes in GitHub pipelines (dotTEST Autofix feature) + +## Table of Contents + +- [dotTEST capabilities](#dotTEST-capabilities) +- [About WebGoat.NET project](#WebGoatNET-version-03) + +## dotTEST Capabilities + +### Static Analysis + +Static analysis and flow analysis help you verify code quality and ensure compliance with industry standards, such as CWE or OWASP. Static analysis is a software testing method that examines the source code without executing it to detect errors, vulnerabilities, and violations of coding standards. Flow analysis refers to the examination and evaluation of data or control flow within a program or system to identify potential issues such as resource leaks, dead code, security vulnerabilities, or performance bottlenecks. +See [Parasoft dotTEST User Guide](https://docs.parasoft.com/display/DOTTEST20252) for details regarding static and flow analysis with dotTEST as well as other dotTEST capabilities. + +### Run Parasoft dotTEST GitHub Action + +The `Run Parasoft dotTEST` action enables you to run code analysis with dotTEST and review analysis results directly on GitHub. To launch code analysis with dotTEST, add the `Run Parasoft dotTEST` action to your GitHub workflow. [The example](https://github.com/parasoft/run-dottest-action/blob/master/samples/run-dottest-analyzer-template.yml) illustrates a simple workflow consisting of one job "run-dottest-action". + +See [Run dotTEST Action @ GitHub Marketplace](https://github.com/marketplace/actions/run-parasoft-dottest) for details regarding configuration and usage. +See also [Run dotTEST Action project](https://github.com/parasoft/run-dottest-action). + + +### Autofix in CI/CD Using Aider + +`DottestAutoFix` is a Python-based script that leverages AI-powered code analysis to automatically fix dotTEST violations in your .NET projects, based on a generated analysis report. Once a fix is applied, the plugin validates it using `dottestcli` and then adds a commit to the current branch in your project repository. +The following example shows a simple Autofix execution: +```batch +@REM Execute autofix with recommended settings +python DottestAutoFix.py ^ + --report ".dottest/report/report.xml" ^ + --max-attempts 3 ^ + --solution BankExample.sln ^ + --tool-home "C:\Program Files\Parasoft\dotTEST\2025.2" +``` + +See [**LINK**](https://docs.parasoft.com/display/DOTTEST20252/Fixing+Violations+Using+AI+Autofix) for details regarding Autofix configuration and usage. + +## WebGoat.NET version 0.3 + +### Build Status ![build .NET 8](https://github.com/tobyash86/WebGoat.NET/workflows/build%20.NET%208/badge.svg) -## The next generation of the WebGoat example project to demonstrate OWASP TOP 10 vulnerabilities +### The Next-Generation WebGoat Example Project Demonstrating OWASP Top 10 Vulnerabilities This is a re-implementation of the original [WebGoat project for .NET](https://github.com/rappayne/WebGoat.NET). -This web application is a learning platform that attempts to teach about +This web application is a learning platform that attempts to explain common web security flaws. It contains generic security flaws that apply to -most web applications. It also contains lessons that specifically pertain to -the .NET framework. The exercises in this app are intended to teach about -web security attacks and how developers can overcome them. +most web applications. It also includes lessons that specifically pertain to +the .NET framework. The exercises in this app are intended to demonstrate +web security attacks and show how developers can overcome them. -### WARNING!: +#### WARNING!: THIS WEB APPLICATION CONTAINS NUMEROUS SECURITY VULNERABILITIES -WHICH WILL RENDER YOUR COMPUTER VERY INSECURE WHILE RUNNING! IT IS HIGHLY -RECOMMENDED TO COMPLETELY DISCONNECT YOUR COMPUTER FROM ALL NETWORKS WHILE -RUNNING! +WHICH WILL RENDER YOUR COMPUTER VERY INSECURE WHILE RUNNING. IT IS HIGHLY +RECOMMENDED TO COMPLETELY DISCONNECT YOUR COMPUTER FROM ALL NETWORKS DURING USE. -### Notes: +#### Notes: - Google Chrome performs filtering for reflected XSS attacks. These attacks - will not work unless chrome is run with the argument + will not execute unless Chrome is run with the argument `--disable-xss-auditor`. -## Requirements +### Requirements - .NET 8 SDK -## How to build and run +### Building and Running the WebGoat.NET Example -### 1. Running in a Docker container +#### 1. Running the Example in a Docker Container The provided Dockerfile is compatible with both Linux and Windows containers. To build a Docker image, execute the following command: @@ -39,9 +81,9 @@ To build a Docker image, execute the following command: docker build --pull --rm -t webgoat.net . ``` -Please note that Linux image is already built by pipeline and can be pulled from [here](https://github.com/users/tobyash86/packages?repo_name=WebGoat.NET). +Please note that the Linux image is already built by the pipeline and can be pulled from [here](https://github.com/users/tobyash86/packages?repo_name=WebGoat.NET). -#### Linux containers +##### Linux Containers To run the `webgoat.net` image, execute the following command: @@ -49,11 +91,11 @@ To run the `webgoat.net` image, execute the following command: docker run --rm -d -p 5000:80 --name webgoat.net webgoat.net ``` -WebGoat.NET website should be accessible at http://localhost:5000. +The WebGoat.NET website should be accessible at http://localhost:5000. -#### Windows containers +##### Windows Containers -To run `webgoat.net` image, execute the following command: +To run the `webgoat.net` image, execute the following command: ```sh docker run --rm --name webgoat.net webgoat.net @@ -78,7 +120,7 @@ Ethernet adapter Ethernet: In the above example, you can access the WebGoat.NETCore website at http://172.29.245.43. -#### Stopping Docker container +##### Stopping the Docker Container To stop the `webgoat.net` container, execute the following command: @@ -86,9 +128,9 @@ To stop the `webgoat.net` container, execute the following command: docker stop webgoat.net ``` -### 2. Run locally using dotnet.exe (Kestrel) +#### 2. Running the Example Locally Using dotnet.exe (Kestrel) -1. Build and publish WebGoat.NET with the following command: +1. Build and publish WebGoat.NET using the following command: ```sh dotnet publish -c release -o ./app @@ -96,24 +138,33 @@ dotnet publish -c release -o ./app The web application will be deployed to the `app` folder in the current directory. -2. Execute the web application on localhost with the following command: +2. Execute the web application on localhost using the following command: ```sh dotnet ./app/WebGoat.NET.dll --urls=http://localhost:5000 ``` -The the WebGoat.NET website will be accessible at the URL specified with the `--urls` parameter: http://localhost:5000. +The WebGoat.NET website will be accessible at the URL specified with the `--urls` parameter: http://localhost:5000. + +#### 3. Running the Example Using a Script +The WebGoat.NET project ships with scripts that allow you to conveniently run the web application. The following scripts are located in the `script` directory at the root of the project: +- runInDocker.bat - runs the application in a Docker container on Windows. +- runInDocker.sh - runs the application in a Docker container on Linux. +- runLocal.bat - runs the application locally on Windows. +- runLocal.sh - runs the application locally on Linux. + +### Known Issues: + +1. The latest OWASP Top 10 is not covered. The missing vulnerabilities need to be added to the codebase. +2. Educational documents and training materials for any categories of the latest OWASP Top 10 are not available. + + + + + + -### 3. Run using a script -The WebGoat.NET projects ships with scripts that allow you to conveniently run the web application. The following scripts are located in the the "script" directory in the root of the project: -- runInDocker.bat - Runs the application in a Docker container on Windows. -- runInDocker.sh - Runs the application in a Docker container on Linux. -- runLocal.bat - Runs the application locally on Windows. -- runLocal.sh - Runs the application locally on Linux. -## Known issues: -1. The latest OWASP Top 10 is not covered. The uncovered vulnerabilities need to be added to the code base. -2. Educational documents/trainings for any categories of the latest OWASP Top 10 are not available.