diff --git a/Astrapia/components/Graph.vue b/Astrapia/components/Graph.vue
index 15120c1..7abead0 100644
--- a/Astrapia/components/Graph.vue
+++ b/Astrapia/components/Graph.vue
@@ -167,12 +167,14 @@ export default {
this.node = this.node
.data(nodes, d => d.id)
- .join(enter => enter.append("circle")
- .attr("r", 8)
- .attr("fill", d => d.hexColor ?? "#537B87")
- .call(this.drag(this.simulation))
- .call(node => node.append("title")
- .text(d => `IP: ${d.name} ${d.tags ? `\nTags: ${d.tags}` : ''}`))
+ .join(
+ enter => enter.append("circle")
+ .attr("r", 8)
+ .attr("fill", d => d.hexColor ?? "#537B87")
+ .call(this.drag(this.simulation))
+ .call(node => node.append("title")
+ .text(d => `IP: ${d.name} ${d.tags ? `\nTags: ${d.tags}` : ''}`)),
+ update => update.attr("fill", d => d.hexColor ?? "#537B87")
);
this.label = this.label
@@ -255,7 +257,7 @@ export default {
if (selectedNode) {
this.node.transition()
.duration(150)
- .attr("fill", "#537B87")
+ .attr("fill", d => d.hexColor ?? "#537B87")
.attr("r", 8)
.attr("opacity", 1);
this.link.transition()
diff --git a/Astrapia/components/GraphFilterMenu.vue b/Astrapia/components/GraphFilterMenu.vue
index 9bb8540..3fe0a2c 100644
--- a/Astrapia/components/GraphFilterMenu.vue
+++ b/Astrapia/components/GraphFilterMenu.vue
@@ -21,7 +21,8 @@ const props = defineProps({
const emit = defineEmits<{
layersFetched: [],
- menuOpened: [boolean]
+ menuOpened: [boolean],
+ queryConditions: [any]
}>();
const graphFilterMenuState = ref({
@@ -35,6 +36,7 @@ const graphFilterMenuState = ref({
async function getLayersOfLayout() {
const layoutData = await layoutService.getLayoutByName(graphFilterMenuState.value.selectedLayout.name);
graphFilterMenuState.value.selectedLayout.layers = layoutData.layers;
+ emit('queryConditions', layoutData.queryConditions);
emit('layersFetched');
}
diff --git a/Astrapia/components/QueryConditionButton.vue b/Astrapia/components/QueryConditionButton.vue
new file mode 100644
index 0000000..04a05e7
--- /dev/null
+++ b/Astrapia/components/QueryConditionButton.vue
@@ -0,0 +1,65 @@
+
+
+
+
Query Conditions:
+
+
+
+
+
+
+
+
+
diff --git a/Astrapia/components/QueryConditionForm.vue b/Astrapia/components/QueryConditionForm.vue
new file mode 100644
index 0000000..1541bc4
--- /dev/null
+++ b/Astrapia/components/QueryConditionForm.vue
@@ -0,0 +1,337 @@
+
+
+
+
+
+
+
diff --git a/Astrapia/components/conditions/StyleConditionBox.vue b/Astrapia/components/conditions/StyleConditionBox.vue
index 5a6d5f3..e20b970 100644
--- a/Astrapia/components/conditions/StyleConditionBox.vue
+++ b/Astrapia/components/conditions/StyleConditionBox.vue
@@ -74,16 +74,15 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -208,21 +207,21 @@ const filterConditionBoxState = ref({
useProtocolColors: true,
protocolColors: {
Unknown:{
- startHex: "#000000",
- endHex: "#FFFFFF"
+ startHex: "#FFFFFF",
+ endHex: "#000000"
},
Tcp:{
- startHex: "#000000",
- endHex: "#FFFFFF"
+ startHex: "#FFFFFF",
+ endHex: "#000000"
},
Udp:{
- startHex: "#000000",
- endHex: "#FFFFFF"
+ startHex: "#FFFFFF",
+ endHex: "#000000"
},
- // Icmp:{
- // startHex: "#000000",
- // endHex: "#FFFFFF"
- // }
+ Icmp:{
+ startHex: "#FFFFFF",
+ endHex: "#000000"
+ }
}
},
nodeStyler: {
diff --git a/Astrapia/pages/topology.vue b/Astrapia/pages/topology.vue
index 49f45d3..5f19d5e 100644
--- a/Astrapia/pages/topology.vue
+++ b/Astrapia/pages/topology.vue
@@ -2,11 +2,12 @@
@@ -18,11 +19,13 @@ import Graph from "~/components/Graph.vue";
import Dropdown from "~/components/Dropdown.vue";
import GraphFilterMenu from "~/components/GraphFilterMenu.vue";
import TopologyTimeframeSelector from "~/components/TopologyTimeframeSelector.vue";
+import QueryConditionButton from "~/components/QueryConditionButton.vue";
const layout = ref('');
const timeframeSelectorFrom = ref(new Date(new Date().getTime() - 2 * 60 * 1000).toISOString().slice(0,16))
const timeframeSelectorTo = ref(new Date().toISOString().slice(0,16))
const data = ref();
+const queryConditions = ref(null);
const intervalAmount = ref(0);
let fetchInterval: NodeJS.Timeout | null = null;
@@ -34,6 +37,10 @@ const handleTimeframeSelection = (from: string, to: string) => {
fetchAndUpdateGraph();
}
+const handleQueryConditions = (conditions: any) => {
+ queryConditions.value = conditions;
+};
+
const handleIntervalAmount = (amount: number) => {
intervalAmount.value = amount;
if (fetchInterval) {
@@ -119,4 +126,12 @@ onBeforeUnmount(() => {
z-index: 15;
margin: 0.75vh 13vw 0 0;
}
+
+.query-conditions{
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 15;
+ margin: 0.75vh 39vw 0 0;
+}
diff --git a/Astrapia/plugins/fontawesome.js b/Astrapia/plugins/fontawesome.js
index 92a1233..cc2e792 100644
--- a/Astrapia/plugins/fontawesome.js
+++ b/Astrapia/plugins/fontawesome.js
@@ -21,7 +21,8 @@ import {
faMinus,
faFloppyDisk,
faArrowLeft,
- faRightFromBracket
+ faRightFromBracket,
+ faFilter
} from '@fortawesome/free-solid-svg-icons'
library.add(
@@ -45,7 +46,8 @@ library.add(
faMinus,
faFloppyDisk,
faArrowLeft,
- faRightFromBracket
+ faRightFromBracket,
+ faFilter
)
config.autoAddCss = false
diff --git a/Astrapia/services/layoutService.ts b/Astrapia/services/layoutService.ts
index 64c597d..69c7ee7 100644
--- a/Astrapia/services/layoutService.ts
+++ b/Astrapia/services/layoutService.ts
@@ -7,7 +7,13 @@ export interface Layouts {
export interface Layout {
name: string,
- layers: []
+ layers: [],
+ queryConditions: {
+ allowDuplicates: boolean,
+ dataProtocolsWhitelist: [],
+ flowProtocolsWhitelist: [],
+ portsWhitelist: []
+ }
}
class LayoutService {
@@ -30,6 +36,10 @@ class LayoutService {
public updateLayout(name: string, newName: string) {
return ApiService.put(`/api/layout/${name}/${newName}`);
}
+
+ public setQueryConditions(name: string, queryConditions: any){
+ return ApiService.put(`/api/layout/${name}/queryConditions`, queryConditions)
+ }
}
export default new LayoutService();
diff --git a/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs b/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs
index 7c4bc7f..4c69ccf 100644
--- a/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs
+++ b/Packrat/Fennec.Tests/Integration/QueryConditionTests.cs
@@ -54,8 +54,8 @@ public async Task FiltersByPort()
await SeedDatabase();
var conditions = new QueryConditions
{
- FlowProtocolsWhitelist = new[] { FlowProtocol.Ipfix, FlowProtocol.Netflow5 },
- DataProtocolsWhitelist = new[] { DataProtocol.Tcp }
+ FlowProtocolsWhitelist = new List { FlowProtocol.Ipfix, FlowProtocol.Netflow5 },
+ DataProtocolsWhitelist = new List { DataProtocol.Tcp }
};
var service = new TraceRepository(Database, null!, null!);
@@ -77,7 +77,7 @@ public async Task FiltersByDuplicateAndPort()
var conditions = new QueryConditions
{
AllowDuplicates = false,
- PortsWhitelist = new[] { 10, 50 }
+ PortsWhitelist = new List { 10, 50 }
};
var service = new TraceRepository(Database, null!, null!);
diff --git a/Packrat/Fennec/Controllers/LayoutController.cs b/Packrat/Fennec/Controllers/LayoutController.cs
index 536d7cc..574ebbd 100644
--- a/Packrat/Fennec/Controllers/LayoutController.cs
+++ b/Packrat/Fennec/Controllers/LayoutController.cs
@@ -50,6 +50,9 @@ public async Task List()
[SwaggerResponse(StatusCodes.Status400BadRequest, "A layout with the name already exists")]
public async Task Create(string name)
{
+ if (!ModelState.IsValid)
+ return BadRequest();
+
try
{
var layout = await _layoutRepository.CreateLayout(name);
@@ -68,7 +71,7 @@ public async Task Create(string name)
///
///
[HttpGet("{name}")]
- [SwaggerResponse(StatusCodes.Status200OK, "Layout successfully returned", typeof(Layout))]
+ [SwaggerResponse(StatusCodes.Status200OK, "Layout successfully returned", typeof(FullLayoutDto))]
[SwaggerResponse(StatusCodes.Status404NotFound, "The layout with the name does not exist")]
public async Task Get(string name)
{
@@ -137,14 +140,20 @@ public async Task Delete(string name)
[HttpPut("{name}/queryConditions")]
[SwaggerResponse(StatusCodes.Status200OK, "Query conditions successfully updated", typeof(FullLayoutDto))]
[SwaggerResponse(StatusCodes.Status404NotFound, "The layout with the name does not exist")]
- public async Task ReplaceQueryConditions(string name, QueryConditionsDto queryConditions)
+ public async Task ReplaceQueryConditions(string name, [FromBody] QueryConditionsDto queryConditions)
{
+ if (!ModelState.IsValid)
+ return BadRequest();
+
var layout = await _layoutRepository.GetLayout(name);
if (layout == null)
return NotFound($"The layout with the name `{name}` does not exist.");
var newQueryConditions = _mapper.Map(queryConditions);
await _layoutRepository.ReplaceQueryConditions(name, newQueryConditions);
+
+ // TODO: update it using the previous call instead of re-fetching
+ layout = await _layoutRepository.GetLayout(name);
return Ok(_mapper.Map(layout));
}
}
diff --git a/Packrat/Fennec/Database/Domain/Layout.cs b/Packrat/Fennec/Database/Domain/Layout.cs
index bc606c1..5d9cb04 100644
--- a/Packrat/Fennec/Database/Domain/Layout.cs
+++ b/Packrat/Fennec/Database/Domain/Layout.cs
@@ -27,28 +27,28 @@ public class QueryConditions
///
/// This is intended to help differentiate between SFlow and other flow protocols.
[BsonElement("flowProtocolsWhitelist")]
- public FlowProtocol[]? FlowProtocolsWhitelist { get; set; }
+ public List? FlowProtocolsWhitelist { get; set; }
///
/// A list of allowed only those data carrying protocols that should be included in the result. If null this
/// condition is ignored.
///
[BsonElement("dataProtocolsWhitelist")]
- public DataProtocol[]? DataProtocolsWhitelist { get; set; }
+ public List? DataProtocolsWhitelist { get; set; }
///
/// If specified a list of allowed source and destination ports that should be included in the result. If either
/// source or destination port matches any of the ports in the list the trace is included in the result.
///
[BsonElement("portsWhitelist")]
- public int[]? PortsWhitelist { get; set; }
+ public List? PortsWhitelist { get; set; }
}
public record QueryConditionsDto(
bool? AllowDuplicates,
- FlowProtocol[]? FlowProtocolsWhitelist,
- DataProtocol[]? DataProtocolsWhitelist,
- int[]? PortsWhitelist);
+ List? FlowProtocolsWhitelist,
+ List? DataProtocolsWhitelist,
+ List? PortsWhitelist);
///
/// Represents a list of steps that should be taken before sending data to the frontend.
@@ -59,6 +59,7 @@ public Layout(string name)
{
Name = name;
Layers = new List();
+ QueryConditions = new QueryConditions();
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
@@ -78,7 +79,7 @@ protected Layout() { }
///
/// The conditions that should be applied when querying the database.
///
- [BsonElement("queryConditions")]
+ [BsonElement("queryConditions")]
public QueryConditions QueryConditions { get; set; } = new();
///
diff --git a/Packrat/Fennec/Database/MapperProfile.cs b/Packrat/Fennec/Database/MapperProfile.cs
index bcaaac5..77a3320 100644
--- a/Packrat/Fennec/Database/MapperProfile.cs
+++ b/Packrat/Fennec/Database/MapperProfile.cs
@@ -12,16 +12,16 @@ public MapperProfile()
// TODO: rethink all this dto madness
CreateMap()
.ConvertUsing(ip => ip.ToString());
-
+
CreateMap()
.ConvertUsing(str => IPAddress.Parse(str));
-
+
CreateMap()
.ConvertUsing(str => IPAddress.Parse(str).GetAddressBytes());
-
+
CreateMap()
.ConvertUsing(bytes => new IPAddress(bytes).ToString());
-
+
CreateMap()
.ConstructUsing((dto, _) =>
{
@@ -63,36 +63,48 @@ public MapperProfile()
CreateMap();
CreateMap();
-
+
CreateMap()
.ConstructUsing((dto, ctx) =>
{
if (!LayerType.LookupTable.TryGetValue(dto.Type, out var layerType))
throw new ArgumentException("No layer type found in the lookup table.");
-
- return (ILayer) ctx.Mapper.Map(dto, dto.GetType(), layerType.LayerType);
+
+ return (ILayer)ctx.Mapper.Map(dto, dto.GetType(), layerType.LayerType);
});
-
+
CreateMap()
.ConstructUsing((layer, ctx) =>
{
if (!LayerType.LookupTable.TryGetValue(layer.Type, out var layerType))
throw new ArgumentException("No layer type found in the lookup table.");
-
- return (ILayerDto) ctx.Mapper.Map(layer, layer.GetType(), layerType.DtoType);
+
+ return (ILayerDto)ctx.Mapper.Map(layer, layer.GetType(), layerType.DtoType);
});
CreateMap()
- .ForCtorParam("LayerCount", opt =>
+ .ForCtorParam("LayerCount", opt =>
opt.MapFrom(src => src.Layers.Count));
+ CreateMap()
+ .ForAllMembers(opts => opts.AllowNull());
- CreateMap();
CreateMap()
- .ForAllMembers(opts => opts.AllowNull());
+ .ConstructUsing((q, _) =>
+ new QueryConditionsDto(q.AllowDuplicates, q.FlowProtocolsWhitelist, q.DataProtocolsWhitelist,
+ q.PortsWhitelist))
+ .ForAllMembers(o => o.AllowNull());
CreateMap()
- .ForAllMembers(opts => opts.AllowNull());
+ .ConstructUsing(q =>
+ new QueryConditions
+ {
+ AllowDuplicates = q.AllowDuplicates,
+ FlowProtocolsWhitelist = q.FlowProtocolsWhitelist,
+ DataProtocolsWhitelist = q.DataProtocolsWhitelist,
+ PortsWhitelist = q.PortsWhitelist
+ })
+ .ForAllMembers(o => o.AllowNull());
CreateMap();
CreateMap();
@@ -101,7 +113,7 @@ public MapperProfile()
CreateMap();
CreateMap();
-
+
CreateMap();
CreateMap();
CreateMap();
diff --git a/Packrat/Fennec/Startup.cs b/Packrat/Fennec/Startup.cs
index 1a518a2..c24c11d 100644
--- a/Packrat/Fennec/Startup.cs
+++ b/Packrat/Fennec/Startup.cs
@@ -1,6 +1,7 @@
using System.Data;
using System.Net.Http.Headers;
using System.Text;
+using System.Text.Json.Serialization;
using Fennec.Database;
using Fennec.Database.Domain;
using Fennec.Metrics;
@@ -49,10 +50,7 @@ public void ConfigureServices(IServiceCollection services, IWebHostEnvironment e
{
BsonSerializer.RegisterSerializer(typeof(ILayer), new MongoLayerSerializer());
BsonSerializer.RegisterSerializer(typeof(Dictionary), new ProtocolColorsDictionarySerializer());
- JsonConvert.DefaultSettings = () => new JsonSerializerSettings
- {
- Converters = { new StringEnumConverter() }
- };
+
services.AddAutoMapper(typeof(MapperProfile));
@@ -174,8 +172,14 @@ public void ConfigureServices(IServiceCollection services, IWebHostEnvironment e
Log.Error("Failed to read multiplexer configuration... To run no multiplexers define an empty list");
// Web services
- services.AddControllers(c => { c.ModelBinderProviders.Insert(0, new LayerModelBinderProvider()); })
- .AddNewtonsoftJson(options => { options.SerializerSettings.Converters.Add(new StringEnumConverter()); });
+ services
+ .AddControllers(c => { c.ModelBinderProviders.Insert(0, new LayerModelBinderProvider()); })
+ .AddNewtonsoftJson(options =>
+ {
+ options.SerializerSettings.NullValueHandling = NullValueHandling.Include;
+ options.SerializerSettings.Converters.Add(new StringEnumConverter());
+ });
+
services.AddAutoMapper(typeof(Program).Assembly);
if (StartupOptions.AllowCors)