feat(new tool): IP Subnets Exclude Calculator

Fix #1386
This commit is contained in:
sharevb 2025-01-12 14:56:25 +01:00 committed by ShareVB
parent 80e46c9292
commit d2b692269b
8 changed files with 402 additions and 80 deletions

View file

@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as ipIncludeExclude } from './ip-include-exclude';
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
import { tool as numeronymGenerator } from './numeronym-generator';
import { tool as macAddressGenerator } from './mac-address-generator';
@ -143,7 +144,15 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Network',
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator],
components: [
ipv4SubnetCalculator,
ipv4AddressConverter,
ipv4RangeExpander,
ipIncludeExclude,
macAddressLookup,
macAddressGenerator,
ipv6UlaGenerator,
],
},
{
name: 'Math',

View file

@ -0,0 +1,12 @@
import { UnfoldMoreOutlined } from '@vicons/material';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'IP Subnets Exclude Calculator',
path: '/ip-include-exclude',
description: 'Substract a disallowed IP Ranges/Mask/CIDR list from an allowed IP Ranges/Mask/CIDR list',
keywords: ['ip', 'allowed', 'disallowed', 'include', 'exclude', 'subnet', 'cidr'],
component: () => import('./ip-include-exclude.vue'),
icon: UnfoldMoreOutlined,
createdAt: new Date('2024-08-15'),
});

View file

@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest';
import { substractCIDRs } from './ip-include-exclude.service';
describe('ip-include-exclude', () => {
describe('substractCIDRs', () => {
it('should return error on invalid values', () => {
expect(substractCIDRs({ allowedRanges: '192.168.0.', disallowedRanges: '' })).to.deep.eq({ // NOSONAR
allowedCIDRs: [],
allowedSubnets: [],
disallowedSubnets: [],
error: 'Error: Invalid IP (range/subnetwork)',
});
});
it('should return correct substractions and subnets', () => {
expect(substractCIDRs({ allowedRanges: '192.168.3.0/24', disallowedRanges: '' })).to.deep.eq({ // NOSONAR
allowedCIDRs: ['192.168.3.0/24'], // NOSONAR
allowedSubnets: [{ cidr: '192.168.3.0/24', end: '192.168.3.255', start: '192.168.3.0' }], // NOSONAR
disallowedSubnets: [],
error: '',
});
expect(substractCIDRs({ allowedRanges: '192.168.2.0/24', disallowedRanges: '192.168.2.1' })).to.deep.eq({ // NOSONAR
allowedCIDRs: ['192.168.2.0/32', // NOSONAR
'192.168.2.2/31', // NOSONAR
'192.168.2.4/30', // NOSONAR
'192.168.2.8/29', // NOSONAR
'192.168.2.16/28', // NOSONAR
'192.168.2.32/27', // NOSONAR
'192.168.2.64/26', // NOSONAR
'192.168.2.128/25'], // NOSONAR
allowedSubnets: [
{ cidr: '192.168.2.0/24', end: '192.168.2.255', start: '192.168.2.0' }, // NOSONAR
],
disallowedSubnets: [
{ cidr: '192.168.2.1/32', end: '192.168.2.1', start: '192.168.2.1' }, // NOSONAR
],
error: '',
});
expect(substractCIDRs({ allowedRanges: '192.168.12.0/24', disallowedRanges: '192.168.12.1-192.168.12.10, 192.168.12.34' })).to.deep.eq({ // NOSONAR
allowedCIDRs: ['192.168.12.0/32', // NOSONAR
'192.168.12.11/32', // NOSONAR
'192.168.12.12/30', // NOSONAR
'192.168.12.16/28', // NOSONAR
'192.168.12.32/31', // NOSONAR
'192.168.12.35/32', // NOSONAR
'192.168.12.36/30', // NOSONAR
'192.168.12.40/29', // NOSONAR
'192.168.12.48/28', // NOSONAR
'192.168.12.64/26', // NOSONAR
'192.168.12.128/25'], // NOSONAR
allowedSubnets: [{ cidr: '192.168.12.0/24', end: '192.168.12.255', start: '192.168.12.0' }], // NOSONAR
disallowedSubnets: [
{ cidr: '192.168.12.1/32', end: '192.168.12.1', start: '192.168.12.1' }, // NOSONAR
{ cidr: '192.168.12.2/31', end: '192.168.12.3', start: '192.168.12.2' }, // NOSONAR
{ cidr: '192.168.12.4/30', end: '192.168.12.7', start: '192.168.12.4' }, // NOSONAR
{ cidr: '192.168.12.8/31', end: '192.168.12.9', start: '192.168.12.8' }, // NOSONAR
{ cidr: '192.168.12.10/32', end: '192.168.12.10', start: '192.168.12.10' }, // NOSONAR
{ cidr: '192.168.12.34/32', end: '192.168.12.34', start: '192.168.12.34' }, // NOSONAR
],
error: '',
});
});
});
});

View file

@ -0,0 +1,49 @@
import type { IPMask } from 'ip-matching';
import { getMatch } from 'ip-matching';
import { excludeCidr } from 'cidr-tools';
function convertToCIDR(mask: IPMask) {
const subnet = mask.convertToSubnet();
if (!subnet) {
return { cidr: mask.toString(), start: '', end: '' };
}
return {
cidr: subnet.toString(),
start: subnet.getFirst().toString(),
end: subnet.getLast().toString(),
};
}
export function substractCIDRs(
{ allowedRanges, disallowedRanges }:
{
allowedRanges: string
disallowedRanges: string
}) {
try {
const allowedRangesMatchMasks = allowedRanges.split(/\s*[,;|]+\s*/g) // NOSONAR
.filter(range => range)
.flatMap(range => getMatch(range)?.convertToMasks() || []);
const disallowedRangesMatchMasks = disallowedRanges.split(/\s*[,;|]+\s*/g) // NOSONAR
.filter(range => range)
.flatMap(range => getMatch(range)?.convertToMasks() || []);
const allowedSubnets = allowedRangesMatchMasks.map(convertToCIDR);
const disallowedSubnets = disallowedRangesMatchMasks.map(convertToCIDR);
return {
error: '',
allowedSubnets,
disallowedSubnets,
allowedCIDRs: excludeCidr(allowedSubnets.map(net => net.cidr), disallowedSubnets.map(net => net.cidr)),
};
}
catch (e: any) {
return {
error: e.toString(),
allowedSubnets: [],
disallowedSubnets: [],
allowedCIDRs: [],
};
}
}

View file

@ -0,0 +1,80 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core';
import { substractCIDRs } from './ip-include-exclude.service';
import SpanCopyable from '@/components/SpanCopyable.vue';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
const allowedRanges = useStorage('ip-inc-exc:allow', '192.168.0.1/24'); // NOSONAR
const disallowedRanges = useStorage('ip-inc-exc:disallow', '192.168.0.6'); // NOSONAR
const result = computed(() => substractCIDRs({
allowedRanges: allowedRanges.value, disallowedRanges: disallowedRanges.value,
}));
</script>
<template>
<div>
<c-input-text
v-model:value="allowedRanges"
label="AllowedIPs (IPv4/6 CIDR/Range/Mask/Wildcard)"
placeholder="An IPv4/6 CIDR/Range/Mask/Wildcard..."
mb-4
/>
<c-input-text
v-model:value="disallowedRanges"
label="DisallowedIPs (IPv4/6 CIDR/Range/Mask/Wildcard)"
placeholder="An IPv4/6 CIDR/Range/Mask/Wildcard..."
mb-4
/>
<n-divider />
<c-alert v-if="result.error">
{{ result.error }}
</c-alert>
<div v-if="!result.error">
<n-form-item label="Final AllowedIPs:">
<TextareaCopyable :value="result.allowedCIDRs.join(', ')" />
</n-form-item>
<n-divider />
<c-card title="Allowed Subnets">
<n-table>
<tbody>
<tr v-for="{ cidr, start, end } in result.allowedSubnets" :key="cidr">
<td font-bold>
<SpanCopyable :value="cidr" />
</td>
<td>
<SpanCopyable :value="start" />
</td>
<td>
<SpanCopyable :value="end" />
</td>
</tr>
</tbody>
</n-table>
</c-card>
<c-card title="Disallowed Subnets">
<n-table>
<tbody>
<tr v-for="{ cidr, start, end } in result.disallowedSubnets" :key="cidr">
<td font-bold>
<SpanCopyable :value="cidr" />
</td>
<td>
<SpanCopyable :value="start" />
</td>
<td>
<SpanCopyable :value="end" />
</td>
</tr>
</tbody>
</n-table>
</c-card>
</div>
</div>
</template>