- Add MIB files from LibreNMS in priv/mibs/ for reference - Create MibParser module to validate OIDs against official MIB definitions - Add MIB validation tests to ensure hardcoded OIDs match MIB specs - Refactor SNMP tests to be generic/behavior-focused instead of vendor-specific - Remove vendor-specific test files (cisco_test, net_snmp_test) - All 104 tests passing with automated OID validation
172 lines
4.7 KiB
Elixir
172 lines
4.7 KiB
Elixir
defmodule Towerops.Snmp.MibParser do
|
|
@moduledoc """
|
|
Simple MIB file parser for extracting OID definitions.
|
|
|
|
This parser extracts OID assignments from MIB files for validation purposes.
|
|
It's not a full ASN.1 parser - just enough to validate our hardcoded OIDs
|
|
against the official MIB definitions.
|
|
|
|
## Usage
|
|
|
|
iex> MibParser.parse_mib_file("priv/mibs/standard/IF-MIB")
|
|
{:ok, %{
|
|
"ifIndex" => "1.3.6.1.2.1.2.2.1.1",
|
|
"ifDescr" => "1.3.6.1.2.1.2.2.1.2",
|
|
...
|
|
}}
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Parse a MIB file and return a map of object names to OID strings.
|
|
"""
|
|
@spec parse_mib_file(String.t()) :: {:ok, %{String.t() => String.t()}} | {:error, term()}
|
|
def parse_mib_file(path) do
|
|
case File.read(path) do
|
|
{:ok, content} ->
|
|
oids = parse_mib_content(content)
|
|
{:ok, oids}
|
|
|
|
{:error, reason} = error ->
|
|
Logger.error("Failed to read MIB file #{path}: #{inspect(reason)}")
|
|
error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Parse MIB content and extract OID definitions.
|
|
"""
|
|
@spec parse_mib_content(String.t()) :: %{String.t() => String.t()}
|
|
def parse_mib_content(content) do
|
|
# First, build a map of object assignments (e.g., "ifTable ::= { interfaces 2 }")
|
|
assignments = extract_assignments(content)
|
|
|
|
# Then resolve all OIDs by following the assignment chain
|
|
resolve_oids(assignments)
|
|
end
|
|
|
|
@doc """
|
|
Validate that a given OID matches the definition in a MIB file.
|
|
"""
|
|
@spec validate_oid(String.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
|
|
def validate_oid(mib_file, object_name, expected_oid) do
|
|
case parse_mib_file(mib_file) do
|
|
{:ok, oids} ->
|
|
case Map.get(oids, object_name) do
|
|
^expected_oid ->
|
|
:ok
|
|
|
|
actual_oid when is_binary(actual_oid) ->
|
|
{:error, "OID mismatch for #{object_name}: expected #{expected_oid}, got #{actual_oid}"}
|
|
|
|
nil ->
|
|
{:error, "Object #{object_name} not found in MIB"}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, "Failed to parse MIB: #{inspect(reason)}"}
|
|
end
|
|
end
|
|
|
|
# Private functions
|
|
|
|
defp extract_assignments(content) do
|
|
# Match patterns like:
|
|
# objectName OBJECT-TYPE ::= { parent index }
|
|
# objectName OBJECT IDENTIFIER ::= { parent index }
|
|
# We'll extract: objectName, parent, index
|
|
|
|
# Remove comments (lines starting with --)
|
|
content = Regex.replace(~r/--.*$/m, content, "")
|
|
|
|
# Match assignment patterns
|
|
# This regex captures: name, parent, and index
|
|
~r/(\w+)\s+(?:OBJECT-TYPE|OBJECT\s+IDENTIFIER)[^:]*::=\s*\{\s*(\w+)\s+(\d+)\s*\}/
|
|
|> Regex.scan(content)
|
|
|> Map.new(fn [_, name, parent, index] ->
|
|
{name, {parent, index}}
|
|
end)
|
|
end
|
|
|
|
defp resolve_oids(assignments) do
|
|
# Well-known root OIDs
|
|
roots = %{
|
|
"iso" => "1",
|
|
"org" => "1.3",
|
|
"dod" => "1.3.6",
|
|
"internet" => "1.3.6.1",
|
|
"directory" => "1.3.6.1.1",
|
|
"mgmt" => "1.3.6.1.2",
|
|
"mib-2" => "1.3.6.1.2.1",
|
|
"experimental" => "1.3.6.1.3",
|
|
"private" => "1.3.6.1.4",
|
|
"enterprises" => "1.3.6.1.4.1",
|
|
"security" => "1.3.6.1.5",
|
|
"snmpV2" => "1.3.6.1.6",
|
|
"snmpModules" => "1.3.6.1.6.3"
|
|
}
|
|
|
|
# Build OID map by recursively resolving assignments
|
|
Enum.reduce(assignments, roots, fn {name, {parent, index}}, acc ->
|
|
case resolve_oid(parent, index, acc, assignments) do
|
|
{:ok, oid} -> Map.put(acc, name, oid)
|
|
:error -> acc
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp resolve_oid(parent, index, resolved, assignments) do
|
|
cond do
|
|
# Parent already resolved
|
|
Map.has_key?(resolved, parent) ->
|
|
{:ok, "#{resolved[parent]}.#{index}"}
|
|
|
|
# Parent needs to be resolved from assignments
|
|
Map.has_key?(assignments, parent) ->
|
|
{grandparent, parent_index} = assignments[parent]
|
|
|
|
case resolve_oid(grandparent, parent_index, resolved, assignments) do
|
|
{:ok, parent_oid} ->
|
|
{:ok, "#{parent_oid}.#{index}"}
|
|
|
|
:error ->
|
|
:error
|
|
end
|
|
|
|
# Can't resolve
|
|
true ->
|
|
:error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
List all MIB files in the priv/mibs directory.
|
|
"""
|
|
@spec list_mib_files() :: [String.t()]
|
|
def list_mib_files do
|
|
mibs_dir = Application.app_dir(:towerops, "priv/mibs")
|
|
|
|
case File.ls(mibs_dir) do
|
|
{:ok, subdirs} ->
|
|
Enum.flat_map(subdirs, &list_mib_files_in_subdir(mibs_dir, &1))
|
|
|
|
{:error, _} ->
|
|
[]
|
|
end
|
|
end
|
|
|
|
defp list_mib_files_in_subdir(mibs_dir, subdir) do
|
|
subdir_path = Path.join(mibs_dir, subdir)
|
|
|
|
case File.ls(subdir_path) do
|
|
{:ok, files} ->
|
|
files
|
|
|> Enum.filter(&String.ends_with?(&1, "MIB"))
|
|
|> Enum.map(&Path.join([mibs_dir, subdir, &1]))
|
|
|
|
{:error, _} ->
|
|
[]
|
|
end
|
|
end
|
|
end
|