feat: add full Zonemaster stack with Docker and Spanish UI
- Clone all 5 Zonemaster component repos (LDNS, Engine, CLI, Backend, GUI) - Dockerfile.backend: 8-stage multi-stage build LDNS→Engine→CLI→Backend - Dockerfile.gui: Astro static build served via nginx - docker-compose.yml: backend (internal) + frontend (port 5353) - nginx.conf: root redirects to /es/, /api/ proxied to backend - zonemaster-gui/config.ts: defaultLanguage set to 'es' (Spanish) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
973
zonemaster-engine/lib/Zonemaster/Engine/Profile.pm
Normal file
973
zonemaster-engine/lib/Zonemaster/Engine/Profile.pm
Normal file
@@ -0,0 +1,973 @@
|
||||
package Zonemaster::Engine::Profile;
|
||||
|
||||
use v5.16.0;
|
||||
use warnings;
|
||||
|
||||
use version; our $VERSION = version->declare( "v1.2.22" );
|
||||
|
||||
use File::ShareDir qw[dist_file];
|
||||
use JSON::PP qw( encode_json decode_json );
|
||||
use Scalar::Util qw(reftype looks_like_number);
|
||||
use File::Slurp;
|
||||
use Clone qw(clone);
|
||||
use Data::Dumper;
|
||||
use Net::IP::XS;
|
||||
use Log::Any qw( $log );
|
||||
use YAML::XS qw();
|
||||
|
||||
$YAML::XS::Boolean = "JSON::PP";
|
||||
|
||||
use Zonemaster::Engine::Constants qw( $DURATION_5_MINUTES_IN_SECONDS $DURATION_1_HOUR_IN_SECONDS $DURATION_4_HOURS_IN_SECONDS $DURATION_12_HOURS_IN_SECONDS $DURATION_1_DAY_IN_SECONDS $DURATION_1_WEEK_IN_SECONDS $DURATION_180_DAYS_IN_SECONDS );
|
||||
use Zonemaster::Engine::Validation qw( validate_ipv4 validate_ipv6 );
|
||||
|
||||
my %profile_properties_details = (
|
||||
q{cache} => {
|
||||
type => q{HashRef},
|
||||
test => sub {
|
||||
my @allowed_keys = ( 'redis' );
|
||||
foreach my $cache_database ( keys %{$_[0]} ) {
|
||||
if ( not grep( /^$cache_database$/, @allowed_keys ) ) {
|
||||
die "Property cache keys have " . scalar @allowed_keys . " possible values: " . join(", ", @allowed_keys) . "\n";
|
||||
}
|
||||
|
||||
if ( not scalar keys %{ $_[0]->{$cache_database} } ) {
|
||||
die "Property cache.$cache_database has no items\n";
|
||||
}
|
||||
else {
|
||||
my @allowed_subkeys;
|
||||
if ( $cache_database eq 'redis' ) {
|
||||
@allowed_subkeys = ( 'server', 'expire' );
|
||||
}
|
||||
|
||||
foreach my $key ( keys %{ $_[0]->{$cache_database} } ) {
|
||||
if ( not grep( /^$key$/, @allowed_subkeys ) ) {
|
||||
die "Property cache.$cache_database subkeys have " . scalar @allowed_subkeys . " possible values: " . join(", ", @allowed_subkeys) . "\n";
|
||||
}
|
||||
|
||||
die "Property cache.$cache_database.$key has a NULL or empty item\n" if not $_[0]->{$cache_database}->{$key};
|
||||
die "Property cache.$cache_database.$key has a negative value\n" if ( looks_like_number( $_[0]->{$cache_database}->{$key} ) and $_[0]->{$cache_database}->{$key} < 0 ) ;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
default => {},
|
||||
},
|
||||
q{resolver.defaults.debug} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{resolver.defaults.igntc} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{resolver.defaults.fallback} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{resolver.defaults.recurse} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{resolver.defaults.retrans} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
max => 255
|
||||
},
|
||||
q{resolver.defaults.retry} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
max => 255
|
||||
},
|
||||
q{resolver.defaults.usevc} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{resolver.defaults.timeout} => {
|
||||
type => q{Num}
|
||||
},
|
||||
q{resolver.source4} => {
|
||||
type => q{Str},
|
||||
test => sub {
|
||||
unless ( $_[0] eq '' or validate_ipv4( $_[0] ) ) {
|
||||
die "Property resolver.source4 must be an IPv4 address or the empty string\n";
|
||||
}
|
||||
},
|
||||
default => q{}
|
||||
},
|
||||
q{resolver.source6} => {
|
||||
type => q{Str},
|
||||
test => sub {
|
||||
unless ( $_[0] eq '' or validate_ipv6( $_[0] ) ) {
|
||||
die "Property resolver.source6 must be a valid IPv6 address or the empty string\n";
|
||||
}
|
||||
},
|
||||
default => q{}
|
||||
},
|
||||
q{net.ipv4} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{net.ipv6} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{no_network} => {
|
||||
type => q{Bool}
|
||||
},
|
||||
q{asn_db.style} => {
|
||||
type => q{Str},
|
||||
test => sub {
|
||||
if ( lc($_[0]) ne q{cymru} and lc($_[0]) ne q{ripe} ) {
|
||||
die "Property asn_db.style has 2 possible values : Cymru or RIPE (case-insensitive)\n";
|
||||
}
|
||||
$_[0] = lc($_[0]);
|
||||
},
|
||||
default => q{cymru}
|
||||
},
|
||||
q{asn_db.sources} => {
|
||||
type => q{HashRef},
|
||||
test => sub {
|
||||
foreach my $db_style ( keys %{$_[0]} ) {
|
||||
if ( lc($db_style) ne q{cymru} and lc($db_style) ne q{ripe} ) {
|
||||
die "Property asn_db.sources keys have 2 possible values : Cymru or RIPE (case-insensitive)\n";
|
||||
}
|
||||
if ( not scalar @{ ${$_[0]}{$db_style} } ) {
|
||||
die "Property asn_db.sources.$db_style has no items\n";
|
||||
}
|
||||
else {
|
||||
foreach my $ndd ( @{ ${$_[0]}{$db_style} } ) {
|
||||
die "Property asn_db.sources.$db_style has a NULL item\n" if not defined $ndd;
|
||||
die "Property asn_db.sources.$db_style has a non scalar item\n" if not defined ref($ndd);
|
||||
die "Property asn_db.sources.$db_style has an item too long\n" if length($ndd) > 255;
|
||||
foreach my $label ( split /[.]/, $ndd ) {
|
||||
die "Property asn_db.sources.$db_style has a non domain name item\n" if $label !~ /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
|
||||
}
|
||||
}
|
||||
${$_[0]}{lc($db_style)} = delete ${$_[0]}{$db_style};
|
||||
}
|
||||
}
|
||||
},
|
||||
default => { cymru => [ "asnlookup.zonemaster.net" ] },
|
||||
},
|
||||
q{logfilter} => {
|
||||
type => q{HashRef},
|
||||
default => {}
|
||||
},
|
||||
q{test_levels} => {
|
||||
type => q{HashRef}
|
||||
},
|
||||
q{test_cases} => {
|
||||
type => q{ArrayRef}
|
||||
},
|
||||
q{test_cases_vars.dnssec04.REMAINING_SHORT} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_12_HOURS_IN_SECONDS
|
||||
},
|
||||
q{test_cases_vars.dnssec04.REMAINING_LONG} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_180_DAYS_IN_SECONDS
|
||||
},
|
||||
q{test_cases_vars.dnssec04.DURATION_LONG} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_180_DAYS_IN_SECONDS
|
||||
},
|
||||
q{test_cases_vars.zone02.SOA_REFRESH_MINIMUM_VALUE} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_4_HOURS_IN_SECONDS
|
||||
},
|
||||
q{test_cases_vars.zone04.SOA_RETRY_MINIMUM_VALUE} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_1_HOUR_IN_SECONDS
|
||||
},
|
||||
q{test_cases_vars.zone05.SOA_EXPIRE_MINIMUM_VALUE} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_1_WEEK_IN_SECONDS
|
||||
},
|
||||
q{test_cases_vars.zone06.SOA_DEFAULT_TTL_MAXIMUM_VALUE} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_1_DAY_IN_SECONDS
|
||||
},
|
||||
q{test_cases_vars.zone06.SOA_DEFAULT_TTL_MINIMUM_VALUE} => {
|
||||
type => q{Num},
|
||||
min => 1,
|
||||
default => $DURATION_5_MINUTES_IN_SECONDS
|
||||
}
|
||||
);
|
||||
|
||||
_init_profile_properties_details_defaults();
|
||||
|
||||
sub _init_profile_properties_details_defaults {
|
||||
my $default_file = dist_file( 'Zonemaster-Engine', 'profile.json');
|
||||
my $json = read_file( $default_file );
|
||||
my $default_values = decode_json( $json );
|
||||
foreach my $property_name ( keys %profile_properties_details ) {
|
||||
if ( defined _get_value_from_nested_hash( $default_values, split /[.]/, $property_name ) ) {
|
||||
$profile_properties_details{$property_name}{default} = clone _get_value_from_nested_hash( $default_values, split /[.]/, $property_name );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _get_profile_paths {
|
||||
my ( $paths_ref, $data, @path ) = @_;
|
||||
|
||||
foreach my $key (sort keys %$data) {
|
||||
|
||||
my $path = join '.', @path, $key;
|
||||
if (ref($data->{$key}) eq 'HASH' and not exists $profile_properties_details{$path} ) {
|
||||
_get_profile_paths($paths_ref, $data->{$key}, @path, $key);
|
||||
next;
|
||||
}
|
||||
else {
|
||||
$paths_ref->{$path} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _get_value_from_nested_hash {
|
||||
my ( $hash_ref, @path ) = @_;
|
||||
|
||||
my $key = shift @path;
|
||||
if ( exists $hash_ref->{$key} ) {
|
||||
if ( @path ) {
|
||||
my $value_type = reftype($hash_ref->{$key});
|
||||
if ( $value_type eq q{HASH} ) {
|
||||
return _get_value_from_nested_hash( $hash_ref->{$key}, @path );
|
||||
}
|
||||
else {
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return $hash_ref->{$key};
|
||||
}
|
||||
}
|
||||
else {
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
|
||||
sub _set_value_to_nested_hash {
|
||||
my ( $hash_ref, $value, @path ) = @_;
|
||||
|
||||
my $key = shift @path;
|
||||
|
||||
if ( ! exists $hash_ref->{$key} ) {
|
||||
$hash_ref->{$key} = {};
|
||||
}
|
||||
if ( @path ) {
|
||||
_set_value_to_nested_hash( $hash_ref->{$key}, $value, @path );
|
||||
}
|
||||
else {
|
||||
$hash_ref->{$key} = clone $value;
|
||||
}
|
||||
}
|
||||
|
||||
our $effective = Zonemaster::Engine::Profile->default;
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $self = {};
|
||||
$self->{q{profile}} = {};
|
||||
|
||||
bless $self, $class;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub default {
|
||||
my ( $class ) = @_;
|
||||
my $new = $class->new;
|
||||
foreach my $property_name ( keys %profile_properties_details ) {
|
||||
if ( exists $profile_properties_details{$property_name}{default} ) {
|
||||
$new->set( $property_name, $profile_properties_details{$property_name}{default} );
|
||||
}
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
sub all_properties {
|
||||
my ( $class ) = @_;
|
||||
return sort keys %profile_properties_details;
|
||||
}
|
||||
|
||||
sub get {
|
||||
my ( $self, $property_name ) = @_;
|
||||
|
||||
die "Unknown property '$property_name'\n" if not exists $profile_properties_details{$property_name};
|
||||
|
||||
if ( $profile_properties_details{$property_name}->{type} eq q{ArrayRef} or $profile_properties_details{$property_name}->{type} eq q{HashRef} ) {
|
||||
return clone _get_value_from_nested_hash( $self->{q{profile}}, split /[.]/, $property_name );
|
||||
} else {
|
||||
return _get_value_from_nested_hash( $self->{q{profile}}, split /[.]/, $property_name );
|
||||
}
|
||||
}
|
||||
|
||||
sub set {
|
||||
my ( $self, $property_name, $value ) = @_;
|
||||
|
||||
$self->_set( q{DIRECT}, $property_name, $value );
|
||||
}
|
||||
|
||||
sub _set {
|
||||
my ( $self, $from, $property_name, $value ) = @_;
|
||||
my $value_type = reftype($value);
|
||||
my $data_details;
|
||||
|
||||
die "Unknown property '$property_name'\n" if not exists $profile_properties_details{$property_name};
|
||||
|
||||
$data_details = sprintf "[TYPE=%s][FROM=%s][VALUE_TYPE=%s][VALUE=%s]\n%s",
|
||||
exists $profile_properties_details{$property_name}->{type} ? $profile_properties_details{$property_name}->{type} : q{UNDEF},
|
||||
defined $from ? $from : q{UNDEF},
|
||||
defined $value_type ? $value_type : q{UNDEF},
|
||||
defined $value ? $value : q{[UNDEF]},
|
||||
Data::Dumper::Dumper($value);
|
||||
# $value is a Scalar
|
||||
if ( ! $value_type or $value_type eq q{SCALAR} ) {
|
||||
die "Property $property_name can not be undef\n" if not defined $value;
|
||||
|
||||
# Boolean
|
||||
if ( $profile_properties_details{$property_name}->{type} eq q{Bool} ) {
|
||||
if ( $from eq q{DIRECT} and !$value ) {
|
||||
$value = JSON::PP::false;
|
||||
}
|
||||
elsif ( $from eq q{DIRECT} and $value ) {
|
||||
$value = JSON::PP::true;
|
||||
}
|
||||
elsif ( $from eq q{JSON} and $value_type and $value == JSON::PP::false ) {
|
||||
$value = JSON::PP::false;
|
||||
}
|
||||
elsif ( $from eq q{JSON} and $value_type and $value == JSON::PP::true ) {
|
||||
$value = JSON::PP::true;
|
||||
}
|
||||
else {
|
||||
die "Property $property_name is of type Boolean $data_details\n";
|
||||
}
|
||||
}
|
||||
# Number. In our case, only non-negative integers
|
||||
elsif ( $profile_properties_details{$property_name}->{type} eq q{Num} ) {
|
||||
if ( $value !~ /^(\d+)$/ ) {
|
||||
die "Property $property_name is of type non-negative integer $data_details\n";
|
||||
}
|
||||
if ( exists $profile_properties_details{$property_name}->{min} and $value < $profile_properties_details{$property_name}->{min} ) {
|
||||
die "Property $property_name value is out of limit (smaller)\n";
|
||||
}
|
||||
if ( exists $profile_properties_details{$property_name}->{max} and $value > $profile_properties_details{$property_name}->{max} ) {
|
||||
die "Property $property_name value is out of limit (bigger)\n";
|
||||
}
|
||||
|
||||
$value = 0+ $value; # Make sure JSON::PP doesn't serialize it as a JSON string
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Array
|
||||
if ( $profile_properties_details{$property_name}->{type} eq q{ArrayRef} and reftype($value) ne q{ARRAY} ) {
|
||||
die "Property $property_name is not a ArrayRef $data_details\n";
|
||||
}
|
||||
# Hash
|
||||
elsif ( $profile_properties_details{$property_name}->{type} eq q{HashRef} and reftype($value) ne q{HASH} ) {
|
||||
die "Property $property_name is not a HashRef $data_details\n";
|
||||
}
|
||||
elsif ( $profile_properties_details{$property_name}->{type} eq q{Bool} or $profile_properties_details{$property_name}->{type} eq q{Num} or $profile_properties_details{$property_name}->{type} eq q{Str} ) {
|
||||
die "Property $property_name is a Scalar $data_details\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ( $profile_properties_details{$property_name}->{test} ) {
|
||||
$profile_properties_details{$property_name}->{test}->( $value );
|
||||
}
|
||||
|
||||
return _set_value_to_nested_hash( $self->{q{profile}}, $value, split /[.]/, $property_name );
|
||||
}
|
||||
|
||||
sub merge {
|
||||
my ( $self, $other_profile ) = @_;
|
||||
|
||||
die "Merge with ", __PACKAGE__, " only\n" if ref($other_profile) ne __PACKAGE__;
|
||||
|
||||
foreach my $property_name ( keys %profile_properties_details ) {
|
||||
if ( defined _get_value_from_nested_hash( $other_profile->{q{profile}}, split /[.]/, $property_name ) ) {
|
||||
$self->_set( q{JSON}, $property_name, _get_value_from_nested_hash( $other_profile->{q{profile}}, split /[.]/, $property_name ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $other_profile->{q{profile}};
|
||||
}
|
||||
|
||||
sub from_json {
|
||||
my ( $class, $json ) = @_;
|
||||
my $new = $class->new;
|
||||
my $internal = decode_json( $json );
|
||||
my %paths;
|
||||
_get_profile_paths(\%paths, $internal);
|
||||
foreach my $property_name ( keys %paths ) {
|
||||
if ( defined _get_value_from_nested_hash( $internal, split /[.]/, $property_name ) ) {
|
||||
$new->_set( q{JSON}, $property_name, _get_value_from_nested_hash( $internal, split /[.]/, $property_name ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
sub to_json {
|
||||
my ( $self ) = @_;
|
||||
|
||||
return encode_json( $self->{q{profile}} );
|
||||
}
|
||||
|
||||
sub from_yaml {
|
||||
my ( $class, $yaml ) = @_;
|
||||
my $data = YAML::XS::Load( $yaml );
|
||||
return $class->from_json( encode_json( $data ) );
|
||||
}
|
||||
|
||||
sub to_yaml {
|
||||
my ( $self ) = @_;
|
||||
|
||||
return YAML::XS::Dump( $self->{q{profile}} );
|
||||
}
|
||||
|
||||
sub effective {
|
||||
return $effective;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Zonemaster::Engine::Profile - A simple system for configuring Zonemaster Engine
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
This module has two parts:
|
||||
|
||||
=over
|
||||
|
||||
=item * a I<profile> representation class
|
||||
|
||||
=item * a global profile object (the I<effective profile>) that configures Zonemaster Engine
|
||||
|
||||
=back
|
||||
|
||||
A I<profile> consists of a collection of named properties.
|
||||
|
||||
The properties determine the configurable behaviors of Zonemaster
|
||||
Engine with regard to what tests are to be performed, how they are to
|
||||
be performed, and how the results are to be analyzed.
|
||||
For details on available properties see the L</PROFILE PROPERTIES>
|
||||
section.
|
||||
|
||||
Here is an example for updating the effective profile with values from
|
||||
a given file and setting all properties not mentioned in the file to
|
||||
default values.
|
||||
For details on the file format see the L</REPRESENTATIONS> section.
|
||||
|
||||
use Zonemaster::Engine::Profile;
|
||||
|
||||
my $json = read_file( "/path/to/foo.profile" );
|
||||
my $foo = Zonemaster::Engine::Profile->from_json( $json );
|
||||
my $profile = Zonemaster::Engine::Profile->default;
|
||||
$profile->merge( $foo );
|
||||
Zonemaster::Engine::Profile->effective->merge( $profile );
|
||||
|
||||
Here is an example for serializing the default profile to JSON.
|
||||
|
||||
my $string = Zonemaster::Engine::Profile->default->to_json;
|
||||
|
||||
For any given profile:
|
||||
|
||||
=over
|
||||
|
||||
=item * At any moment, each property is either set or unset.
|
||||
|
||||
=item * At any moment, every set property has a valid value.
|
||||
|
||||
=item * It is possible to set the value of each unset property.
|
||||
|
||||
=item * It is possible to update the value of each set property.
|
||||
|
||||
=item * It is NOT possible to unset the value of any set property.
|
||||
|
||||
=back
|
||||
|
||||
=head1 CLASS ATTRIBUTES
|
||||
|
||||
=head2 effective
|
||||
|
||||
A L<Zonemaster::Engine::Profile>.
|
||||
This is the effective profile.
|
||||
It serves as the global runtime configuration for Zonemaster Engine.
|
||||
Update it to change the configuration.
|
||||
|
||||
The effective profile is initialized with the default values declared
|
||||
in the L</PROFILE PROPERTIES> section.
|
||||
|
||||
All properties in the effective profile are always set (to valid values).
|
||||
|
||||
=head1 CLASS METHODS
|
||||
|
||||
=head2 new
|
||||
|
||||
A constructor that returns a new profile with all properties unset.
|
||||
|
||||
my $profile = Zonemaster::Engine::Profile->new;
|
||||
|
||||
=head2 default
|
||||
|
||||
A constructor that returns a new profile with the default property
|
||||
values declared in the L</PROFILE PROPERTIES> section.
|
||||
|
||||
my $default = Zonemaster::Engine::Profile->default;
|
||||
|
||||
=head2 from_json
|
||||
|
||||
A constructor that returns a new profile with values parsed from a JSON string.
|
||||
|
||||
my $profile = Zonemaster::Engine::Profile->from_json( '{ "no_network": true }' );
|
||||
|
||||
The returned profile has set values for all properties specified in the
|
||||
given string.
|
||||
The remaining properties are unset.
|
||||
|
||||
Dies if the given string is illegal according to the L</JSON REPRESENTATION>
|
||||
section or if the property values are illegal according to the L</PROFILE
|
||||
PROPERTIES> section.
|
||||
|
||||
=head2 from_yaml
|
||||
|
||||
A constructor that returns a new profile with values parsed from a YAML string.
|
||||
|
||||
my $profile = Zonemaster::Engine::Profile->from_yaml( <<EOF
|
||||
no_network: true
|
||||
EOF
|
||||
);
|
||||
|
||||
The returned profile has set values for all properties specified in the
|
||||
given string.
|
||||
The remaining properties are unset.
|
||||
|
||||
Dies if the given string is illegal according to the L</YAML REPRESENTATION>
|
||||
section or if the property values are illegal according to the L</PROFILE
|
||||
PROPERTIES> section.
|
||||
|
||||
=head1 INSTANCE METHODS
|
||||
|
||||
=head2 get
|
||||
|
||||
Get the value of a property.
|
||||
|
||||
my $value = $profile1->get( 'net.ipv6' );
|
||||
|
||||
Returns value of the given property, or C<undef> if the property is unset.
|
||||
For boolean properties the returned value is either C<1> for true or C<0> for
|
||||
false.
|
||||
For properties with complex types, the returned value is a
|
||||
L<deep copy|https://en.wiktionary.org/wiki/deep_copy#Noun>.
|
||||
|
||||
Dies if the given property name is invalid.
|
||||
|
||||
=head2 set
|
||||
|
||||
Set the value of a property.
|
||||
|
||||
$profile1->set( 'net.ipv6', 0 );
|
||||
|
||||
Takes a property name and value and updates the property accordingly.
|
||||
For boolean properties any truthy value is interpreted as true and any falsy
|
||||
value except C<undef> is interpreted as false.
|
||||
|
||||
Dies if the given property name is invalid.
|
||||
|
||||
Dies if the value is C<undef> or otherwise invalid for the given property.
|
||||
|
||||
=head2 merge
|
||||
|
||||
Merge the profile data of another profile into this one.
|
||||
|
||||
$profile1->merge( $other );
|
||||
|
||||
Properties from the other profile take precedence when the same property
|
||||
name exists in both profiles.
|
||||
The other profile object remains unmodified.
|
||||
|
||||
=head2 to_json
|
||||
|
||||
Serialize the profile to the L</JSON REPRESENTATION> format.
|
||||
|
||||
my $string = $profile->to_json();
|
||||
|
||||
Returns a string.
|
||||
|
||||
=head2 to_yaml
|
||||
|
||||
Serialize the profile to the L</JSON REPRESENTATION> format.
|
||||
|
||||
my $string = $profile->to_yaml();
|
||||
|
||||
Returns a string.
|
||||
|
||||
=head2 all_properties
|
||||
|
||||
Get the names of all properties.
|
||||
|
||||
Returns a sorted list of strings.
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=head2 _get_profile_paths
|
||||
|
||||
Internal method used to get all the paths of a nested hashes-of-hashes.
|
||||
It creates a hash where keys are dotted keys of the nested hashes-of-hashes
|
||||
that exist in %profile_properties_details.
|
||||
|
||||
_get_profile_paths(\%paths, $internal);
|
||||
|
||||
=head2 _get_value_from_nested_hash
|
||||
|
||||
Internal method used to get a value in a nested hashes-of-hashes.
|
||||
|
||||
_get_value_from_nested_hash( $hash_ref, @path );
|
||||
|
||||
Where $hash_ref is the hash to explore and @path are the labels of the property to get.
|
||||
|
||||
@path = split /\./, q{resolver.defaults.usevc};
|
||||
|
||||
=head2 _set_value_to_nested_hash
|
||||
|
||||
Internal method used to set a value in a nested hashes-of-hashes.
|
||||
|
||||
_set_value_from_nested_hash( $hash_ref, $value, @path );
|
||||
|
||||
Where $hash_ref is the hash to explore and @path are the labels of the property to set.
|
||||
|
||||
@path = split /\./, q{resolver.defaults.usevc};
|
||||
|
||||
=head1 PROFILE PROPERTIES
|
||||
|
||||
Each property has a name and is either set or unset.
|
||||
If it is set it has a value that is valid for that specific property.
|
||||
Here is a listing of all the properties and their respective sets of
|
||||
valid values.
|
||||
|
||||
=head2 resolver.defaults.retrans
|
||||
|
||||
An integer between 1 and 255 inclusive. The number of seconds between retries.
|
||||
Default 3.
|
||||
|
||||
=head2 resolver.defaults.retry
|
||||
|
||||
An integer between 1 and 255 inclusive.
|
||||
The number of times a query is sent before we give up. Default 2.
|
||||
|
||||
=head2 resolver.defaults.fallback
|
||||
|
||||
A boolean. If true, UDP queries that get responses with the C<TC>
|
||||
flag set will be automatically resent over TCP or using EDNS. Default
|
||||
true.
|
||||
|
||||
In ldns-1.7.0 (NLnet Labs), in case of truncated answer when UDP is used,
|
||||
the same query is resent with EDNS0 and TCP (if needed). If you
|
||||
want the original answer (with TC bit set) and avoid this kind of
|
||||
replay, set this flag to false.
|
||||
|
||||
=head2 resolver.source4
|
||||
|
||||
A string representation of an IPv4 address or the empty string.
|
||||
The source address all resolver objects should use when sending queries over IPv4.
|
||||
|
||||
If set to "" (empty string), the OS default IPv4 address is used.
|
||||
|
||||
Default: "" (empty string).
|
||||
|
||||
=head2 resolver.source6
|
||||
|
||||
A string representation of an IPv6 address or the empty string.
|
||||
The source address all resolver objects should use when sending queries over IPv6.
|
||||
|
||||
If set to "" (empty string), the OS default IPv6 address is used.
|
||||
|
||||
Default: "" (empty string).
|
||||
|
||||
=head2 resolver.defaults.igntc
|
||||
|
||||
A boolean. Default false. Ignored. Deprecated and planned for removal in v2026.1. Remove it from your profile file.
|
||||
|
||||
=head2 resolver.defaults.recurse
|
||||
|
||||
A boolean. Default false. Ignored. Deprecated and planned for removal in v2026.1. Remove it from your profile file.
|
||||
|
||||
=head2 resolver.defaults.usevc
|
||||
|
||||
A boolean. Default false. Ignored. Deprecated and planned for removal in v2026.1. Remove it from your profile file.
|
||||
|
||||
=head2 net.ipv4
|
||||
|
||||
A boolean. If true, resolver objects are allowed to send queries over
|
||||
IPv4. Default true.
|
||||
|
||||
=head2 net.ipv6
|
||||
|
||||
A boolean. If true, resolver objects are allowed to send queries over
|
||||
IPv6. Default true.
|
||||
|
||||
=head2 no_network
|
||||
|
||||
A boolean. If true, network traffic is forbidden. Default false.
|
||||
|
||||
Use when you want to be sure that any data is only taken from a preloaded
|
||||
cache.
|
||||
|
||||
=head2 asn_db.style
|
||||
|
||||
A string that is either C<"Cymru"> or C<"RIPE"> (case-insensitive).
|
||||
|
||||
Defines which service will be used for AS lookup zones.
|
||||
|
||||
Default C<"Cymru">.
|
||||
|
||||
=head2 asn_db.sources
|
||||
|
||||
A hash of arrayrefs of strings. The currently supported keys are C<"Cymru"> or C<"RIPE"> (case-insensitive).
|
||||
|
||||
For C<"Cymru">, the strings are domain names. For C<"RIPE">, they are WHOIS servers. Normally only the first
|
||||
item in the list will be used, the rest are backups in case the previous ones didn't work.
|
||||
|
||||
Default C<{Cymru: [ "asnlookup.zonemaster.net", "asn.cymru.com" ], RIPE: [ "riswhois.ripe.net" ]}>.
|
||||
|
||||
=head2 cache
|
||||
|
||||
A hash of hashes. The currently supported key is C<"redis">.
|
||||
Default C<{}>.
|
||||
|
||||
=head3 redis
|
||||
|
||||
A hashref. The currently supported keys are C<"server"> and C<"expire">.
|
||||
|
||||
Specifies the address of the Redis server used to perform global caching
|
||||
(C<cache.redis.server>) and an optional expire time (C<cache.redis.expire>).
|
||||
|
||||
C<cache.redis.server> must be a string in the form C<host:port>.
|
||||
C<cache.redis.expire> must be a non-negative integer and defines a time in seconds.
|
||||
Default is 300 seconds.
|
||||
|
||||
=head2 logfilter
|
||||
|
||||
A complex data structure. Default C<{}>.
|
||||
|
||||
Specifies the severity level of each tag emitted by a specific module.
|
||||
The intended use is to remove known erroneous results.
|
||||
E.g. if you know that a certain name server is recursive and for some
|
||||
reason should be, you can use this functionality to lower the severity
|
||||
of the complaint about it to a lower level than normal.
|
||||
The C<test_levels> item also specifies tag severity level, but with
|
||||
coarser granularity and lower precedence.
|
||||
|
||||
The data under the C<logfilter> key should be structured like this:
|
||||
|
||||
Module
|
||||
Tag
|
||||
Array of exceptions
|
||||
"when"
|
||||
Hash with conditions
|
||||
"set"
|
||||
Severity level to set if all conditions match
|
||||
|
||||
The hash with conditions should have keys matching the attributes of
|
||||
the log entry that's being filtered (check the translation files to see
|
||||
what they are). The values for the keys should be either a single value
|
||||
that the attribute should be, or an array of values any one of which the
|
||||
attribute should be.
|
||||
|
||||
A complete logfilter structure might look like this:
|
||||
|
||||
{
|
||||
"A_MODULE": {
|
||||
"SOME_TAG": [
|
||||
{
|
||||
"when": {
|
||||
"count": 1,
|
||||
"type": [
|
||||
"this",
|
||||
"or"
|
||||
]
|
||||
},
|
||||
"set": "INFO"
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"count": 128,
|
||||
"type": [
|
||||
"that"
|
||||
]
|
||||
},
|
||||
"set": "INFO"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ANOTHER_MODULE": {
|
||||
"OTHER_TAG": [
|
||||
{
|
||||
"when": {
|
||||
"bananas": 0
|
||||
},
|
||||
"set": "WARNING"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
This would set the severity level to C<INFO> for any C<A_MODULE:SOME_TAG>
|
||||
messages that had a C<count> attribute set to 1 and a C<type> attribute
|
||||
set to either C<this> or C<or>.
|
||||
This also would set the level to C<INFO> for any C<A_MODULE:SOME_TAG>
|
||||
messages that had a C<count> attribute set to 128 and a C<type> attribute
|
||||
set to C<that>.
|
||||
And this would set the level to C<WARNING> for any C<ANOTHER_MODULE:OTHER_TAG>
|
||||
messages that had a C<bananas> attribute set to 0.
|
||||
|
||||
=head2 test_levels
|
||||
|
||||
A complex data structure.
|
||||
|
||||
Specifies the severity level of each tag emitted by a specific module.
|
||||
The C<logfilter> item also specifies tag severity level, but with finer
|
||||
granularity and higher precedence.
|
||||
|
||||
At the top level of this data structure are two levels of nested hashrefs.
|
||||
The keys of the top level hash are names of test implementation modules
|
||||
(without the C<Zonemaster::Engine::Test::> prefix).
|
||||
The keys of the second level hashes are tags that the respective
|
||||
modules emit.
|
||||
The values of the second level hashes are mapped to severity levels.
|
||||
|
||||
The various L<test case specifications|
|
||||
https://github.com/zonemaster/zonemaster/tree/master/docs/specifications/tests/README.md>
|
||||
define the default severity level for some of the messages.
|
||||
These specifications are the only authoritative documents on the default
|
||||
severity level for the various messages.
|
||||
For messages not defined in any of these specifications you can use the
|
||||
following command to query the default severity level directly from the actual
|
||||
default profile.
|
||||
|
||||
```sh
|
||||
perl -MZonemaster::Engine::Test -E 'say Zonemaster::Engine::Profile->default->to_json' | jq -S .test_levels
|
||||
```
|
||||
|
||||
For messages neither defined in test specifications, nor listed in the default
|
||||
profile, the default severity level is C<DEBUG>.
|
||||
|
||||
I<Note:> Sometimes multiple test cases within the same test module define
|
||||
messages for the same tag.
|
||||
When they do, it is imperative that all test cases define the same severity
|
||||
level for the tag.
|
||||
|
||||
=head2 test_cases
|
||||
|
||||
An arrayref of names of implemented test cases (in all lower-case) as listed in the
|
||||
L<test case specifications|https://github.com/zonemaster/zonemaster/tree/master/docs/specifications/tests/ImplementedTestCases.md>.
|
||||
Default is an arrayref listing all the test cases.
|
||||
|
||||
Specifies which test cases can be run by the testing suite.
|
||||
|
||||
=head2 test_cases_vars.dnssec04.REMAINING_SHORT
|
||||
|
||||
A positive integer value.
|
||||
Recommended lower bound for signatures' remaining validity time (in seconds) in
|
||||
test case L<DNSSEC04|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/DNSSEC-TP/dnssec04.md>.
|
||||
Related to the REMAINING_SHORT message tag from this test case.
|
||||
Default C<43200> (12 hours in seconds).
|
||||
|
||||
=head2 test_cases_vars.dnssec04.REMAINING_LONG
|
||||
|
||||
A positive integer value.
|
||||
Recommended upper bound for signatures' remaining validity time (in seconds) in
|
||||
test case L<DNSSEC04|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/DNSSEC-TP/dnssec04.md>.
|
||||
Related to the REMAINING_LONG message tag from this test case.
|
||||
Default C<15552000> (180 days in seconds).
|
||||
|
||||
=head2 test_cases_vars.dnssec04.DURATION_LONG
|
||||
|
||||
A positive integer value.
|
||||
Recommended upper bound for signatures' lifetime (in seconds) in the test case
|
||||
L<DNSSEC04|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/DNSSEC-TP/dnssec04.md>.
|
||||
Related to the DURATION_LONG message tag from this test case.
|
||||
Default C<15552000> (180 days in seconds).
|
||||
|
||||
=head2 test_cases_vars.zone02.SOA_REFRESH_MINIMUM_VALUE
|
||||
|
||||
A positive integer value.
|
||||
Recommended lower bound for SOA refresh values (in seconds) in test case
|
||||
L<ZONE02|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/Zone-TP/zone02.md>.
|
||||
Related to the REFRESH_MINIMUM_VALUE_LOWER message tag from this test case.
|
||||
Default C<14400> (4 hours in seconds).
|
||||
|
||||
=head2 test_cases_vars.zone04.SOA_RETRY_MINIMUM_VALUE
|
||||
|
||||
A positive integer value.
|
||||
Recommended lower bound for SOA retry values (in seconds) in test case
|
||||
L<ZONE04|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/Zone-TP/zone04.md>.
|
||||
Related to the RETRY_MINIMUM_VALUE_LOWER message tag from this test case.
|
||||
Default C<3600> (1 hour in seconds).
|
||||
|
||||
=head2 test_cases_vars.zone05.SOA_EXPIRE_MINIMUM_VALUE
|
||||
|
||||
A positive integer value.
|
||||
Recommended lower bound for SOA expire values (in seconds) in test case
|
||||
L<ZONE05|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/Zone-TP/zone05.md>.
|
||||
Related to the EXPIRE_MINIMUM_VALUE_LOWER message tag from this test case.
|
||||
Default C<604800> (1 week in seconds).
|
||||
|
||||
=head2 test_cases_vars.zone06.SOA_DEFAULT_TTL_MINIMUM_VALUE
|
||||
|
||||
A positive integer value.
|
||||
Recommended lower bound for SOA minimum values (in seconds) in test case
|
||||
L<ZONE06|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/Zone-TP/zone06.md>.
|
||||
Related to the SOA_DEFAULT_TTL_MAXIMUM_VALUE_LOWER message tag from this test case.
|
||||
Default C<300> (5 minutes in seconds).
|
||||
|
||||
=head2 test_cases_vars.zone06.SOA_DEFAULT_TTL_MAXIMUM_VALUE
|
||||
|
||||
A positive integer value.
|
||||
Recommended upper bound for SOA minimum values (in seconds) in test case
|
||||
L<ZONE06|
|
||||
https://github.com/zonemaster/zonemaster/blob/master/docs/specifications/tests/Zone-TP/zone06.md>.
|
||||
Related to the SOA_DEFAULT_TTL_MAXIMUM_VALUE_HIGHER message tag from this test case.
|
||||
Default C<86400> (1 day in seconds).
|
||||
|
||||
=head1 REPRESENTATIONS
|
||||
|
||||
=head2 JSON REPRESENTATION
|
||||
|
||||
Property names in L</PROFILE PROPERTIES> section correspond to paths in
|
||||
a datastructure of nested JSON objects.
|
||||
Property values are stored at their respective paths.
|
||||
Paths are formed from property names by splitting them at dot characters
|
||||
(U+002E).
|
||||
The left-most path component corresponds to a key in the top-most
|
||||
JSON object.
|
||||
Properties with unset values are omitted in the JSON representation.
|
||||
|
||||
For a complete example, refer to the file located by L<dist_file(
|
||||
"Zonemaster-Engine", "default.profile" )|File::ShareDir/dist_file>.
|
||||
A profile with the only two properties set, C<net.ipv4> = true and
|
||||
C<net.ipv6> = true has this JSON representation:
|
||||
|
||||
{
|
||||
"net": {
|
||||
"ipv4": true,
|
||||
"ipv6": true
|
||||
}
|
||||
}
|
||||
|
||||
=head2 YAML REPRESENTATION
|
||||
|
||||
Similar to the L</JSON REPRESENTATION> but uses a YAML format.
|
||||
|
||||
=cut
|
||||
Reference in New Issue
Block a user