Adding updated version of ProxyHelper, to force client device traffic to Burp Suite Proxy. Based on the original ProxyHelper by @_kc57
parent
52d551f504
commit
58a088d2dd
|
@ -0,0 +1,13 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,46 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"ProxyHelper2": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ProxyHelper2",
|
||||
"sourceRoot": "projects/ProxyHelper2/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-ng-packagr:build",
|
||||
"options": {
|
||||
"tsConfig": "projects/ProxyHelper2/tsconfig.lib.json",
|
||||
"project": "projects/ProxyHelper2/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ProxyHelper2/tsconfig.lib.prod.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "projects/ProxyHelper2/src/test.ts",
|
||||
"tsConfig": "projects/ProxyHelper2/tsconfig.spec.json",
|
||||
"karmaConfig": "projects/ProxyHelper2/karma.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"projects/ProxyHelper2/tsconfig.lib.json",
|
||||
"projects/ProxyHelper2/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}},
|
||||
"defaultProject": "ProxyHelper2"
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
#!/bin/bash
|
||||
|
||||
MODULENAME=$(basename $PWD)
|
||||
|
||||
check_workspace() {
|
||||
if [[ ! -d "node_modules" ]]; then
|
||||
while true; do
|
||||
read -p "[!!] The Angular workspace has not been prepared. Would you like to do it now? [Y\n] " yn
|
||||
case $yn in
|
||||
[Yy]* ) prepare_workspace; break;;
|
||||
[Nn]* ) exit 1;;
|
||||
* ) prepare_workspace; break;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_workspace() {
|
||||
echo "[*] Preparing the Angular workspace."
|
||||
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "[!] NPM does not appear to be installed on this system. Failed to create workspace."
|
||||
return
|
||||
fi
|
||||
|
||||
if ! npm install &> /dev/null; then
|
||||
echo "[!] Failed to prepare workspace. Run npm install to see why."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "[*] Prepared the Angular workspace successfully."
|
||||
}
|
||||
|
||||
build_module() {
|
||||
ng build --prod > /dev/null 2>&1
|
||||
RET=$?
|
||||
|
||||
if [[ $RET -ne 0 ]]; then
|
||||
echo "[!] Angular Build Failed: Run 'ng build --prod' to figure out why."
|
||||
exit 1
|
||||
else
|
||||
echo "[*] Angular Build Succeeded"
|
||||
fi
|
||||
|
||||
# Step 2: Copy the required files to the build output
|
||||
cp -r projects/$MODULENAME/src/module.svg dist/$MODULENAME/bundles/
|
||||
cp -r projects/$MODULENAME/src/module.json dist/$MODULENAME/bundles/
|
||||
cp -r projects/$MODULENAME/src/module.py dist/$MODULENAME/bundles/ > /dev/null 2>&1
|
||||
cp -r projects/$MODULENAME/src/module.php dist/$MODULENAME/bundles/ > /dev/null 2>&1
|
||||
cp -r projects/$MODULENAME/src/assets/ dist/$MODULENAME/bundles/ > /dev/null 2>&1
|
||||
|
||||
# Step 3: Clean up
|
||||
rm -rf dist/$MODULENAME/bundles/*.map
|
||||
rm -rf dist/$MODULENAME/bundles/*.min*
|
||||
rm -rf bundletmp
|
||||
mv dist/$MODULENAME/bundles/ bundletmp
|
||||
rm -rf dist/$MODULENAME/*
|
||||
mv bundletmp/* dist/$MODULENAME/
|
||||
rm -rf bundletmp
|
||||
}
|
||||
|
||||
package() {
|
||||
VERS=$(cat dist/$MODULENAME/module.json | grep "version" | awk '{split($0, a, ": "); gsub("\"", "", a[2]); gsub(",", "", a[2]); print a[2]}')
|
||||
rm -rf $MODULENAME-$VERS.tar.gz
|
||||
echo "[*] Packaging $MODULENAME (Version $VERS)"
|
||||
cd dist/
|
||||
tar -pczf $MODULENAME-$VERS.tar.gz $MODULENAME
|
||||
mv $MODULENAME-$VERS.tar.gz ../
|
||||
cd ../
|
||||
}
|
||||
|
||||
copy_to_device() {
|
||||
echo "[*] Copying module to WiFi Pineapple via SCP"
|
||||
scp -r dist/$MODULENAME root@172.16.42.1:/pineapple/modules
|
||||
}
|
||||
|
||||
main() {
|
||||
check_workspace
|
||||
build_module
|
||||
|
||||
if [[ $1 == "package" ]]; then
|
||||
package
|
||||
elif [[ $1 == "copy" ]]; then
|
||||
copy_to_device
|
||||
fi
|
||||
|
||||
echo "[*] Success!"
|
||||
}
|
||||
|
||||
main $1
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "ProxyHelper2",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~9.1.11",
|
||||
"@angular/cdk": "^9.2.4",
|
||||
"@angular/common": "~9.1.11",
|
||||
"@angular/compiler": "~9.1.11",
|
||||
"@angular/core": "~9.1.11",
|
||||
"@angular/flex-layout": "^9.0.0-beta.31",
|
||||
"@angular/forms": "~9.1.11",
|
||||
"@angular/material": "^9.2.4",
|
||||
"@angular/platform-browser": "~9.1.11",
|
||||
"@angular/platform-browser-dynamic": "~9.1.11",
|
||||
"@angular/router": "~9.1.11",
|
||||
"rxjs": "~6.5.5",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.901.8",
|
||||
"@angular-devkit/build-ng-packagr": "~0.901.8",
|
||||
"@angular/cli": "~9.1.8",
|
||||
"@angular/compiler-cli": "~9.1.11",
|
||||
"@angular/language-service": "~9.1.11",
|
||||
"@types/jasmine": "~3.5.10",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^5.1.2",
|
||||
"jasmine-core": "~3.5.0",
|
||||
"jasmine-spec-reporter": "~5.0.2",
|
||||
"karma": "~5.1.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||
"karma-jasmine": "~3.3.1",
|
||||
"karma-jasmine-html-reporter": "^1.4.0",
|
||||
"ng-packagr": "^9.1.5",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.10.2",
|
||||
"tslint": "~6.1.2",
|
||||
"typescript": "^3.6.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/ProxyHelper2",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "ProxyHelper2",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^8.2.14",
|
||||
"@angular/core": "^8.2.14"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ng build --prod"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
This file is a placeholder for module assets that you may like to add.
|
|
@ -0,0 +1,27 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ProxyHelper2Component } from './components/ProxyHelper2.component';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import {MaterialModule} from './modules/material/material.module';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ProxyHelper2Component }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProxyHelper2Component],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forChild(routes),
|
||||
MaterialModule,
|
||||
FlexLayoutModule,
|
||||
FormsModule,
|
||||
],
|
||||
exports: [ProxyHelper2Component]
|
||||
})
|
||||
export class ProxyHelper2Module { }
|
|
@ -0,0 +1,35 @@
|
|||
.mat-card-content {
|
||||
padding-bottom: 10px; /* Optional: Add padding for spacing */
|
||||
}
|
||||
|
||||
.mat-card-content + .mat-card-content {
|
||||
padding-top: 10px; /* Optional: Add padding for spacing */
|
||||
}
|
||||
|
||||
|
||||
|
||||
.mat-selection-list {
|
||||
border: 1px solid #ccc; /* Border color and width */
|
||||
border-radius: 4px; /* Optional: Add border-radius for rounded corners */
|
||||
padding: 8px; /* Optional: Add padding for spacing */
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.mat-list-option {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.mat-card {
|
||||
margin-bottom: 10px; /* Adjust the value to set the desired bottom margin */
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.mat-flat-button {
|
||||
height: 28px; /* Adjust the height to make the buttons smaller vertically */
|
||||
margin-right: 8px; /* Add spacing to the right of each button */
|
||||
margin-bottom: 8px; /* Add spacing below each button */
|
||||
align-items: center;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<mat-card>
|
||||
<mat-card-title>Welcome to ProxyHelper2</mat-card-title>
|
||||
<mat-card-subtitle>Updated version of Kc57's ProxyHelper</mat-card-subtitle>
|
||||
<mat-card-content>
|
||||
This module sets the iptables rules to force network traffic of client devices to Burp Suite Proxy. This is useful for mobile applications that do not honor proxy settings on the device.<br>
|
||||
Enter the ports you would like to force to Burp Suite Proxy, and the IP and port your Burp Suite Proxy is listening on.<br><br>
|
||||
You'll need to configure your Burp Suite Proxy Listener to listen on all interfaces (not just Loopback only) and set it's Request handling to Support invisible proxying. <br><br>
|
||||
The github repo for this project is here:<br>
|
||||
https://github.com/hoodoer/proxy-helper-the-sequel<br><br>
|
||||
If you have an older pineapple using the older module development style, use the original ProxyHelper module here:<br>
|
||||
https://github.com/trustedsec/proxy_helper<br>
|
||||
https://trustedsec.com/blog/introducing-proxy-helper-a-new-wifi-pineapple-module
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<b>Firewall Rules - Backup and Restore:</b><br><br>
|
||||
<button mat-flat-button color="primary" (click)="saveFirewallRules();">Backup Firewall Rules</button>
|
||||
|
||||
<!-- Loader spinner -->
|
||||
<div *ngIf="!backendLoaded" class="text-center">
|
||||
<mat-spinner diameter="100"></mat-spinner>
|
||||
</div>
|
||||
<br>
|
||||
<ul>
|
||||
<li *ngFor="let backup of backups">
|
||||
<span style="margin-right: 20px;">{{ backup }}</span>
|
||||
<button mat-flat-button color="accent" (click)="restoreBackup(backup)">Restore</button>
|
||||
<button mat-flat-button color="warn" (click)="deleteBackup(backup)">Delete</button>
|
||||
</li>
|
||||
</ul>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<b>TCP Ports to forward to Burp Proxy:</b><br><br>
|
||||
<div *ngFor="let port of ports">
|
||||
<mat-checkbox [(ngModel)]="port.checked" [disabled]="isChecked">{{ port.value }}</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>New Port</mat-label>
|
||||
<input matInput [(ngModel)]="newPort" name="newPort" #newPortInput="ngModel" required pattern="^([1-9]\d{0,4}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$">
|
||||
<mat-error *ngIf="newPortInput.invalid && (newPortInput.dirty || newPortInput.touched)">
|
||||
Please enter a valid port number.
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<br>
|
||||
<button mat-flat-button color="primary" (click)="addNewPort()" [disabled]="newPortInput.invalid || !newPortInput.dirty || isChecked">Add Port</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<b>Burp Proxy Configuration:</b><br>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>IP Address of Proxy</mat-label>
|
||||
<input matInput [(ngModel)]="proxyIpAddress" name="proxyIpAddress" #ipInput="ngModel" [disabled]="isChecked" required pattern="^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])$">
|
||||
<mat-error *ngIf="ipInput.invalid && (ipInput.dirty || ipInput.touched)">
|
||||
Please enter a valid IP address.
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Port of Proxy</mat-label>
|
||||
<input matInput [(ngModel)]="proxyPort" name="proxyPort" #portInput="ngModel" [disabled]="isChecked" required pattern="^([1-9]\d{0,4}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$">
|
||||
<mat-error *ngIf="portInput.invalid && (portInput.dirty || portInput.touched)">
|
||||
Please enter a valid port number.
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<br>
|
||||
<mat-slide-toggle [(ngModel)]="isChecked" (change)="routingToggle()" [disabled]="portInput.invalid || !portInput.dirty || ipInput.invalid || !ipInput.dirty">Route to Proxy</mat-slide-toggle>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<b><i>Contact Info</i></b><br><br>
|
||||
If you're having issues feel free to contact me:<br>
|
||||
@hoodoer<br>
|
||||
hoodoer@bitwisemunitions.dev
|
||||
</mat-card-content>
|
||||
</mat-card>
|
|
@ -0,0 +1,148 @@
|
|||
import { Component, OnInit, ViewEncapsulation, ViewChild } from '@angular/core';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatSelectionList } from '@angular/material/list';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
// import { FormControl, FormGroup, Validators, ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-ProxyHelper2',
|
||||
templateUrl: './ProxyHelper2.component.html',
|
||||
styleUrls: ['./ProxyHelper2.component.css'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
|
||||
|
||||
export class ProxyHelper2Component implements OnInit {
|
||||
constructor(private API: ApiService) { }
|
||||
@ViewChild('portForwardList') portForwardList: MatSelectionList;
|
||||
|
||||
backendLoaded = false;
|
||||
isChecked = false;
|
||||
apiResponse = 'Press the button to get response.';
|
||||
proxyIpAddress = '';
|
||||
proxyPort = '';
|
||||
|
||||
|
||||
|
||||
ports = [
|
||||
{ value: '80', checked: false },
|
||||
{ value: '443', checked: true },
|
||||
];
|
||||
|
||||
newPort: string = '';
|
||||
|
||||
backups: string[] = [];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
saveFirewallRules(): void {
|
||||
this.API.request({
|
||||
module: 'ProxyHelper2',
|
||||
action: 'backupFirewall',
|
||||
}, (response) => {
|
||||
this.apiResponse = response;
|
||||
console.log("Save firewall got response: " + response);
|
||||
console.log("Save firewall error response: " + JSON.stringify(response))
|
||||
this.backups.push(response);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
deleteBackup(backup: string): void {
|
||||
console.log("Should delete backup: " + backup)
|
||||
this.backups = this.backups.filter(i => i !== backup);
|
||||
|
||||
|
||||
this.API.request({
|
||||
module: 'ProxyHelper2',
|
||||
action: 'deleteBackup',
|
||||
backupFile: backup
|
||||
}, (response) => {
|
||||
console.log('Delete backup response: ' + response)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
restoreBackup(backup: string): void {
|
||||
console.log("Would restore firewall backup: " + backup);
|
||||
|
||||
this.API.request({
|
||||
module: 'ProxyHelper2',
|
||||
action: 'restoreFirewall',
|
||||
filename: backup
|
||||
}, (response) => {
|
||||
console.log("Restore response: " + response);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
getBackups(): void {
|
||||
console.log("Fetching list of backups on disk...");
|
||||
|
||||
this.API.request({
|
||||
module: 'ProxyHelper2',
|
||||
action: 'getBackups'
|
||||
}, (response) => {
|
||||
console.log("Get backups response: " + response);
|
||||
console.log(JSON.stringify(response));
|
||||
this.backups.length = 0;
|
||||
|
||||
for (var i = 0; i < response.length; i++) {
|
||||
this.backups.push(response[i]);
|
||||
}
|
||||
this.backendLoaded = true;
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
addNewPort(): void {
|
||||
if (this.newPort && !this.ports.some(port => port.value === this.newPort)) {
|
||||
this.ports.push({ value: this.newPort, checked: false });
|
||||
this.newPort = ''; // Clear the input field after adding a new port
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getSelectedPorts(): string[] {
|
||||
return this.ports.filter(port => port.checked).map(port => port.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
routingToggle(): void {
|
||||
console.log("Routing toggle changed: " + this.isChecked);
|
||||
|
||||
console.log("About to proxy route: " + this.proxyIpAddress + ': ' + this.proxyPort);
|
||||
|
||||
var activePorts = this.getSelectedPorts()
|
||||
|
||||
console.log("Ports to forward: " + activePorts);
|
||||
|
||||
this.API.request({
|
||||
module: 'ProxyHelper2',
|
||||
action: 'routingToggle',
|
||||
toggleValue: this.isChecked,
|
||||
forwardPorts: activePorts,
|
||||
proxyHost: this.proxyIpAddress,
|
||||
proxyPort: this.proxyPort
|
||||
}, (response) => {
|
||||
this.apiResponse = response;
|
||||
console.log("Toggle: " + response);
|
||||
console.log("details: " + JSON.stringify(response));
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.getBackups();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hak5 LLC.
|
||||
*/
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { BidiModule } from '@angular/cdk/bidi';
|
||||
import { ObserversModule } from '@angular/cdk/observers';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { PlatformModule } from '@angular/cdk/platform';
|
||||
import { PortalModule } from '@angular/cdk/portal';
|
||||
import { CdkStepperModule } from '@angular/cdk/stepper';
|
||||
import { CdkTableModule } from '@angular/cdk/table';
|
||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule],
|
||||
exports: [
|
||||
// CDK
|
||||
A11yModule,
|
||||
BidiModule,
|
||||
ObserversModule,
|
||||
OverlayModule,
|
||||
PlatformModule,
|
||||
PortalModule,
|
||||
CdkStepperModule,
|
||||
CdkTableModule,
|
||||
CdkTreeModule,
|
||||
|
||||
// Material
|
||||
MatAutocompleteModule,
|
||||
MatBadgeModule,
|
||||
MatBottomSheetModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatDividerModule,
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatGridListModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatMenuModule,
|
||||
MatNativeDateModule,
|
||||
MatPaginatorModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatRadioModule,
|
||||
MatRippleModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatSnackBarModule,
|
||||
MatSortModule,
|
||||
MatStepperModule,
|
||||
MatTableModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
MatTreeModule,
|
||||
],
|
||||
declarations: []
|
||||
})
|
||||
export class MaterialModule {
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProxyHelper2Service {
|
||||
constructor() {}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
public static totalRequests = 0;
|
||||
apiModuleBusy = document.getElementById('ApiModuleBusy');
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
private router: Router) {}
|
||||
|
||||
emptyResponse = {error: 'Request returned empty response'};
|
||||
|
||||
unauth(): void {
|
||||
localStorage.removeItem('authToken');
|
||||
|
||||
if (this.router.url !== '/Login' && this.router.url !== '/Setup') {
|
||||
this.router.navigateByUrl('/Login');
|
||||
}
|
||||
}
|
||||
|
||||
setBusy(): void {
|
||||
this.apiModuleBusy.style.display = 'block';
|
||||
}
|
||||
setNotBusy(): void {
|
||||
this.apiModuleBusy.style.display = 'none';
|
||||
}
|
||||
|
||||
static extractBaseHref(): string {
|
||||
// Duplicated from injector because we have to be able to support
|
||||
// a static method here
|
||||
if (window['_app_base']) {
|
||||
if (window['_app_base'].endsWith('/')) {
|
||||
return window['_app_base'].slice(0, -1)
|
||||
}
|
||||
}
|
||||
return window['_app_base'] || '';
|
||||
}
|
||||
|
||||
request(payload: any, callback: (any) => void) {
|
||||
this.setBusy();
|
||||
let resp;
|
||||
|
||||
this.http.post(`${ApiService.extractBaseHref()}/api/module/request`, payload).subscribe((r: any) => {
|
||||
if (r === undefined || r === null) {
|
||||
resp = this.emptyResponse;
|
||||
} else if (r.error) {
|
||||
resp = r;
|
||||
} else {
|
||||
resp = r.payload;
|
||||
}
|
||||
}, (err) => {
|
||||
resp = err.error;
|
||||
if (err.status === 401) {
|
||||
this.unauth();
|
||||
}
|
||||
this.setNotBusy();
|
||||
callback(resp);
|
||||
}, () => {
|
||||
this.setNotBusy();
|
||||
callback(resp);
|
||||
});
|
||||
|
||||
ApiService.totalRequests++;
|
||||
}
|
||||
|
||||
APIGet(path: string, callback: (any) => void): any {
|
||||
ApiService.totalRequests++;
|
||||
|
||||
let resp;
|
||||
|
||||
this.http.get(`${ApiService.extractBaseHref()}${path}`).subscribe((r) => {
|
||||
if (r === undefined || r === null) {
|
||||
r = this.emptyResponse;
|
||||
}
|
||||
resp = r;
|
||||
}, (err) => {
|
||||
resp = err.error;
|
||||
if (err.status === 401) {
|
||||
this.unauth();
|
||||
}
|
||||
callback(resp);
|
||||
}, () => {
|
||||
callback(resp);
|
||||
});
|
||||
}
|
||||
|
||||
async APIGetAsync(path: string): Promise<any> {
|
||||
ApiService.totalRequests++;
|
||||
|
||||
return await this.http.get(`${ApiService.extractBaseHref()}${path}`).toPromise();
|
||||
}
|
||||
|
||||
APIPut(path: string, body: any, callback: (any) => void): any {
|
||||
ApiService.totalRequests++;
|
||||
|
||||
let resp;
|
||||
|
||||
this.http.put(`${ApiService.extractBaseHref()}${path}`, body).subscribe((r) => {
|
||||
if (r === undefined || r === null) {
|
||||
r = this.emptyResponse;
|
||||
}
|
||||
resp = r;
|
||||
}, (err) => {
|
||||
resp = err.error;
|
||||
if (err.status === 401) {
|
||||
this.unauth();
|
||||
}
|
||||
callback(resp);
|
||||
}, () => {
|
||||
callback(resp);
|
||||
});
|
||||
}
|
||||
|
||||
async APIPutAsync(path: string, body: any): Promise<any> {
|
||||
return await this.http.put(`${ApiService.extractBaseHref()}/${path}`, body).toPromise();
|
||||
}
|
||||
|
||||
APIPost(path: string, body: any, callback: (any) => void): any {
|
||||
ApiService.totalRequests++;
|
||||
|
||||
let resp;
|
||||
|
||||
this.http.post(`${ApiService.extractBaseHref()}${path}`, body).subscribe((r) => {
|
||||
if (r === undefined || r === null) {
|
||||
resp = this.emptyResponse;
|
||||
}
|
||||
resp = r;
|
||||
}, (err) => {
|
||||
resp = err.error;
|
||||
if (err.status === 401) {
|
||||
this.unauth();
|
||||
}
|
||||
callback(resp);
|
||||
}, () => {
|
||||
callback(resp);
|
||||
});
|
||||
}
|
||||
|
||||
async APIPostAsync(path: string, body: any): Promise<any> {
|
||||
return await this.http.post(`${ApiService.extractBaseHref()}/${path}`, body).toPromise();
|
||||
}
|
||||
|
||||
APIDelete(path: string, body: any, callback: (any) => void): any {
|
||||
ApiService.totalRequests++;
|
||||
|
||||
const opts = {
|
||||
headers: null,
|
||||
body: body
|
||||
};
|
||||
|
||||
let resp;
|
||||
|
||||
this.http.delete(`${ApiService.extractBaseHref()}/${path}`, opts).subscribe((r) => {
|
||||
if (r === undefined || r === null) {
|
||||
r = this.emptyResponse;
|
||||
}
|
||||
resp = r;
|
||||
}, (err) => {
|
||||
resp = err.error;
|
||||
if (err.status === 401) {
|
||||
this.unauth();
|
||||
}
|
||||
callback(resp);
|
||||
}, () => {
|
||||
callback(resp);
|
||||
});
|
||||
}
|
||||
|
||||
async APIDeleteAsync(path: string, body: any): Promise<any> {
|
||||
return await this.http.delete(`${ApiService.extractBaseHref()}${path}`, body).toPromise();
|
||||
}
|
||||
|
||||
APIDownload(fullpath: string, filename: string): void {
|
||||
ApiService.totalRequests++;
|
||||
|
||||
const body = {
|
||||
filename: fullpath
|
||||
};
|
||||
|
||||
this.http.post(`${ApiService.extractBaseHref()}/api/download`, body, {responseType: 'blob'}).subscribe((r) => {
|
||||
const url = window.URL.createObjectURL(r);
|
||||
const a = document.createElement('a');
|
||||
document.body.appendChild(a);
|
||||
a.setAttribute('style', 'display: none');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "ProxyHelper2",
|
||||
"title": "Proxy Helper (The Sequel)",
|
||||
"description": "Module to force client device TCP traffic to Burp Suite Proxy",
|
||||
"version": "1.0",
|
||||
"author": "hoodoer",
|
||||
"firmware_required": "1.0.0",
|
||||
"devices": ["wifipineapplemk7"]
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import pathlib
|
||||
from datetime import datetime
|
||||
from os import listdir
|
||||
|
||||
from pineapple.modules import Module, Request
|
||||
|
||||
|
||||
module = Module('ProxyHelper2', logging.DEBUG)
|
||||
|
||||
|
||||
# This handles the actual forwarding enable/disable
|
||||
@module.handles_action('routingToggle')
|
||||
def routingToggle(request: Request):
|
||||
isChecked = request.toggleValue
|
||||
ports = request.forwardPorts
|
||||
proxyIP = request.proxyHost
|
||||
proxyPort = request.proxyPort
|
||||
|
||||
if isChecked:
|
||||
# Let's enable our forwarding
|
||||
subprocess.run(["echo", "'1'", ">", "/proc/sys/net/ipv4/ip_forward"], shell=False, check=False)
|
||||
|
||||
for port in ports:
|
||||
burpSpot = proxyIP + ":" + proxyPort
|
||||
command = ["iptables", "-t", "nat", "-A", "PREROUTING", "-p", "tcp", "--dport", str(port), "-j", "DNAT", "--to-destination", burpSpot]
|
||||
subprocess.run(command, shell=False, check=False)
|
||||
|
||||
result = subprocess.run(["iptables", "-t", "nat", "-A", "POSTROUTING", "-j", "MASQUERADE"], capture_output=True, text=True, shell=False, check=False)
|
||||
|
||||
return "Forwarding enabled."
|
||||
else:
|
||||
# Turn off forwarding/ clear things
|
||||
|
||||
for port in ports:
|
||||
burpSpot = proxyIP + ":" + proxyPort
|
||||
command = ["iptables", "-t", "nat", "-D", "PREROUTING", "-p", "tcp", "--dport", str(port), "-j", "DNAT", "--to-destination", burpSpot]
|
||||
subprocess.run(command, shell=False, check=False)
|
||||
|
||||
return 'Forwarding cleared.'
|
||||
|
||||
|
||||
|
||||
@module.handles_action('backupFirewall')
|
||||
def backupFirewall(request: Request):
|
||||
current_time = datetime.now()
|
||||
formatted_time = current_time.strftime("%Y-%m-%d_%H_%M_%S")
|
||||
fileName = 'iptables_' + formatted_time;
|
||||
|
||||
# save the stuffs....
|
||||
# These end up in /pineapple/iptablesBackups on the device
|
||||
backupDir = "./iptablesBackups"
|
||||
backupPath = os.path.join(backupDir, fileName)
|
||||
backupCommand = ["iptables-save"]
|
||||
|
||||
if not os.path.exists(backupDir):
|
||||
os.makedirs(backupDir)
|
||||
|
||||
|
||||
with open(backupPath, 'w') as backupFile:
|
||||
subprocess.run(backupCommand, stdout = backupFile, check=True)
|
||||
# result = subprocess.run(backupCommand, check=True, shell=True)
|
||||
|
||||
return fileName
|
||||
|
||||
|
||||
|
||||
@module.handles_action('deleteBackup')
|
||||
def deleteBackup(request: Request):
|
||||
backupFile = request.backupFile
|
||||
|
||||
backupDir = "./iptablesBackups"
|
||||
filePath = os.path.join(backupDir, backupFile)
|
||||
|
||||
deleteCommand = ["rm", filePath]
|
||||
|
||||
try:
|
||||
subprocess.run(" ".join(deleteCommand), shell=True, check=True)
|
||||
return "Delete backup succeeded: " + backupFile
|
||||
except:
|
||||
return "Delete backup failed!" + deleteCommand
|
||||
|
||||
|
||||
|
||||
|
||||
@module.handles_action('restoreFirewall')
|
||||
def restoreFirewall(request: Request):
|
||||
fileName = request.filename
|
||||
backupDir = "./iptablesBackups"
|
||||
restorePath = os.path.join(backupDir, fileName)
|
||||
|
||||
restoreCommand = ["iptables-restore", "<", restorePath]
|
||||
|
||||
try:
|
||||
subprocess.run(" ".join(restoreCommand), shell=True, check=True)
|
||||
return "Restore succeeded: " + fileName
|
||||
except:
|
||||
return "Firewall restore failed!"
|
||||
|
||||
|
||||
|
||||
|
||||
@module.handles_action('getBackups')
|
||||
def getBackups(request: Request):
|
||||
backupDir = "./iptablesBackups"
|
||||
|
||||
backupFiles = os.listdir(backupDir)
|
||||
|
||||
fileTimes = [(file, os.path.getmtime(os.path.join(backupDir, file))) for file in backupFiles]
|
||||
|
||||
sortedBackupFiles = sorted(fileTimes, key=lambda x: x[1])
|
||||
|
||||
cleanedBackupFiles = [filename[0] for filename in sortedBackupFiles]
|
||||
|
||||
return cleanedBackupFiles
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
module.start()
|
|
@ -0,0 +1 @@
|
|||
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M36.7,16.47V11.66c0-3.48-3.13-4.59-5.81-4.67V1.85c7.3,0,11.62,2.49,11.62,9.24v5.35c0,3.42,2.71,5,5.49,5v5.2c-2.78,0-5.49,1.54-5.49,5v5.35c0,6.75-4.32,9.24-11.62,9.24V41c2.68-.08,5.81-1.19,5.81-4.67V31.53c0-4.11,1.75-6.45,4.22-7.53C38.45,22.92,36.7,20.58,36.7,16.47Zm-25.4,0V11.66c0-3.48,3.13-4.59,5.81-4.67V1.85c-7.3,0-11.62,2.49-11.62,9.24v5.35c0,3.42-2.72,5-5.49,5v5.2c2.77,0,5.49,1.54,5.49,5v5.35c0,6.75,4.32,9.24,11.62,9.24V41c-2.68-.08-5.81-1.19-5.81-4.67V31.53c0-4.11-1.75-6.45-4.22-7.53C9.55,22.92,11.3,20.58,11.3,16.47ZM27.38,26.1H20.57v6.48h2.79a4.69,4.69,0,0,1-2.71,4.14l.68,1.46a7.61,7.61,0,0,0,6.05-7.73Zm0-14H20.57v6.81h6.81Z"/></svg>
|
After Width: | Height: | Size: 726 B |
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Public API Surface of ProxyHelper2
|
||||
*/
|
||||
|
||||
export * from './lib/services/ProxyHelper2.service';
|
||||
export * from './lib/components/ProxyHelper2.component';
|
||||
export * from './lib/ProxyHelper2.module';
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"target": "es2015",
|
||||
"declaration": true,
|
||||
"inlineSources": true,
|
||||
"types": [],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2018"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"skipTemplateCodegen": true,
|
||||
"strictMetadataEmit": true,
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true,
|
||||
"enableResourceInlining": true
|
||||
},
|
||||
"exclude": [
|
||||
"src/test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableIvy": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../tslint.json",
|
||||
"rules": {
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"lib",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"lib",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
],
|
||||
"paths": {
|
||||
"ProxyHelper2": [
|
||||
"dist/ProxyHelper2"
|
||||
],
|
||||
"ProxyHelper2/*": [
|
||||
"dist/ProxyHelper2/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"array-type": false,
|
||||
"arrow-parens": false,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"interface-name": false,
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-consecutive-blank-lines": false,
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"object-literal-sort-keys": false,
|
||||
"ordered-imports": false,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"trailing-comma": false,
|
||||
"component-class-suffix": true,
|
||||
"contextual-lifecycle": true,
|
||||
"directive-class-suffix": true,
|
||||
"no-conflicting-lifecycle": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-output-native": true,
|
||||
"no-output-on-prefix": true,
|
||||
"no-output-rename": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue