Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .nuke/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"TestE2E_General",
"TestLocal",
"TestSelf",
"TestUnit"
"TestUnit",
"UpdateOui"
]
},
"Verbosity": {
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="CsvHelper" Version="33.1.0" />
<PackageVersion Include="HLabs.ImageReferences" Version="1.0.0-preview.3" />
<PackageVersion Include="HLabs.ImageReferences.Extensions.Nuke" Version="1.0.0-preview.3" />
<PackageVersion Include="Humanizer" Version="3.0.10" />
Expand Down
139 changes: 139 additions & 0 deletions build/NukeBuild.UpdateOui.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper.Configuration.Attributes;
using Nuke.Common;
using Nuke.Common.IO;
using Serilog;

// ReSharper disable AllUnderscoreLocalParameterName
// ReSharper disable UnusedMember.Local

internal partial class NukeBuild {
private static AbsolutePath OuiGeneratedFile =>
RootDirectory / "src" / "Scanning" / "Oui" / "OuiDatabase.Generated.cs";

private const string OuiCsvUrl = "https://standards-oui.ieee.org/oui/oui.csv";

/// <summary>
/// Downloads the latest IEEE OUI CSV and regenerates OuiDatabase.Generated.cs.
/// </summary>
Target UpdateOui => _ => _
.Executes( async () => {
Log.Information( "Downloading IEEE OUI database from {Url}...", OuiCsvUrl );

using var http = new HttpClient();
var csv = await http.GetStringAsync( OuiCsvUrl );

Log.Information( "Parsing OUI entries..." );

var entries = ParseOuiCsv( csv );

Log.Information( "Parsed {Count} vendor entries", entries.Count );

var source = GenerateSource( entries );

await File.WriteAllTextAsync( OuiGeneratedFile, source,
new UTF8Encoding( encoderShouldEmitUTF8Identifier: false ) );

Log.Information( "Written {Path}", OuiGeneratedFile );
}
);

private sealed class OuiRecord {
public string Registry {
get;
init;
} = "";

public string Assignment {
get;
init;
} = "";

[Name( "Organization Name" )]
public string OrganizationName {
get;
init;
} = "";
}

private static List<(uint, string)> ParseOuiCsv( string csv ) {
var config = new CsvConfiguration( CultureInfo.InvariantCulture ) { HasHeaderRecord = true, };

using var reader = new StringReader( csv );
using var csvReader = new CsvReader( reader, config );

var entries = new List<(uint, string)>();
var seen = new HashSet<uint>();

foreach ( var record in csvReader.GetRecords<OuiRecord>() ) {
// Only MA-L are standard 24-bit OUI assignments
if ( record.Registry != "MA-L" ) {
continue;
}

if ( record.Assignment.Length != 6 ) {
continue;
}

var orgName = record.OrganizationName.Trim();
if ( string.IsNullOrEmpty( orgName ) || orgName == "Private" ) {
continue;
}

if ( !TryParseHex( record.Assignment, out var oui ) ) {
continue;
}

// Deduplicate: keep first occurrence of each OUI
if ( seen.Add( oui ) ) {
entries.Add( ( oui, orgName ) );
}
}

entries.Sort( ( a, b ) => a.Item1.CompareTo( b.Item1 ) );
return entries;
}

private static bool TryParseHex( string hex, out uint result ) {
result = 0;
try {
result = Convert.ToUInt32( hex, 16 );
return true;
}
catch {
return false;
}
}

private static string GenerateSource( List<(uint, string)> entries ) {
var sb = new StringBuilder();
sb.AppendLine( "// Auto-generated from the IEEE OUI database." );
sb.AppendLine( "// Source: https://standards-oui.ieee.org/oui/oui.csv" );
sb.AppendLine( $"// Total entries: {entries.Count}" );
sb.AppendLine( "// To refresh: nuke UpdateOui" );
sb.AppendLine();
sb.AppendLine( "namespace Drift.Scanning.Oui;" );
sb.AppendLine();
sb.AppendLine( "public static partial class OuiDatabase {" );
sb.AppendLine( $" private static readonly Dictionary<uint, string> _vendors = new({entries.Count}) {{" );

foreach ( var entry in entries ) {
var oui = entry.Item1;
var vendor = entry.Item2;
var escaped = vendor.Replace( "\\", "\\\\" ).Replace( "\"", "\\\"" );
sb.AppendLine( $" {{ 0x{oui:X6}u, \"{escaped}\" }}," );
}

sb.AppendLine( " };" );
sb.AppendLine( "}" );

return sb.ToString();
}
}
1 change: 1 addition & 0 deletions build/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CsvHelper" />
<PackageReference Include="HLabs.ImageReferences" />
<PackageReference Include="Octokit" />
<PackageReference Include="Semver" />
Expand Down
3 changes: 2 additions & 1 deletion src/Cli/Commands/Scan/Interactive/Ui/TreeRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,15 @@ private static Tree BuildTree(
}

private static string RenderDevice( Device device, List<Subnet> subnets ) {
var vendor = device.Vendor != null ? $" [grey]{device.Vendor}[/]" : string.Empty;
return
// device.Status + " " +
$"{device.Ip.PadRight( subnets.GetIpWidth() )} " +
$"{device.Mac.PadRight( subnets.GetMacWidth() )} " +
$"{device.Id.PadRight( subnets.GetIdWidth() )} " +
// TODO note not raw version
// $"{device.StateText.PadRightLocal( device.StateText.Length, subnets.GetStateTextWidth() )} " +
device.State.Text + " ";
device.State.Text + vendor + " ";
// "[grey]Few seconds ago[/]";
}
}
5 changes: 5 additions & 0 deletions src/Cli/Commands/Scan/Models/Device.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public required DeviceRenderState State {
init;
}

public string? Vendor {
get;
init;
}

// TODO e.g. "Last seen 5 hours ago""
/*public string Note {
get;
Expand Down
5 changes: 3 additions & 2 deletions src/Cli/Commands/Scan/Rendering/LogScanRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ public void Render( List<Subnet> subnets ) {
log.LogWarning( "Device" );
log.Log(
device.State.State.IsConformant() ? LogLevel.Information : LogLevel.Warning,
"IPv4: {Get}, MAC: {Mac}, Conformant: {Conformant}, State: {State}",
"IPv4: {Ipv4}, MAC: {Mac}, Conformant: {Conformant}, State: {State}, Vendor: {Vendor}",
device.Ip.WithoutMarkup,
device.Mac.WithoutMarkup,
device.State.State.IsConformant(),
device.State.State
device.State.State,
device.Vendor
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Drift.Domain.Device.Discovered;
using Drift.Domain.Extensions;
using Drift.Domain.Scan;
using Drift.Scanning.Oui;
using NaturalSort.Extension;

namespace Drift.Cli.Commands.Scan.ResultProcessors;
Expand Down Expand Up @@ -84,12 +85,15 @@ SubnetScanResult scanResult

var ip = device.Get( AddressType.IpV4 );
var mac = device.Get( AddressType.Mac );
var macAddress = device.Addresses.OfType<MacAddress>().Cast<MacAddress?>().SingleOrDefault();

var deviceRenderState = DeviceRenderState.From( declaredState, discoveredState, unknownAllowed );
deviceRenderState = ip == null || scanResult.DiscoveryAttempts.Contains( new IpV4Address( ip ) )
? deviceRenderState
: new DeviceRenderState( deviceRenderState.State, deviceRenderState.Icon, "[grey bold]Unknown[/]" );

var vendor = macAddress is { } m ? OuiDatabase.LookupVendor( m ) : null;

var id = InteractiveUi.FakeData ? GenerateDeviceId() : declaredDevice?.Id;
var displayId = id == null ? "[grey][/]" : $"[cyan]{id}[/]";

Expand All @@ -106,6 +110,7 @@ SubnetScanResult scanResult
)
),
Id = new DisplayValue( displayId ),
Vendor = vendor,
};
}

Expand Down
Loading
Loading