mirror of
https://github.com/adulau/pdns-toolkit.git
synced 2024-12-22 08:46:00 +00:00
Initial passive DNS server commit at CanSecWest
This commit is contained in:
parent
52da990aad
commit
9be511dc94
15 changed files with 26273 additions and 0 deletions
6
pdns-server/bin/pdns-dnscap2feeder.sh
Normal file
6
pdns-server/bin/pdns-dnscap2feeder.sh
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# A simple feeder using dnscap input
|
||||||
|
#
|
||||||
|
|
||||||
|
./dnscap -T -s r -ieth1 -g 2>&1 >/dev/null | perl pdns-feeder.pl
|
147
pdns-server/bin/pdns-feeder.pl
Normal file
147
pdns-server/bin/pdns-feeder.pl
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
#
|
||||||
|
# Minimal and Scalable Passive DNS - Redis feeder
|
||||||
|
#
|
||||||
|
# Read dnscap output to feed a Redis database
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010-2012 Alexandre Dulaunoy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Date::Manip;
|
||||||
|
use Redis;
|
||||||
|
use Scalar::Util;
|
||||||
|
use IO::Handle;
|
||||||
|
|
||||||
|
$| = 1;
|
||||||
|
|
||||||
|
my $block = 0;
|
||||||
|
my $date;
|
||||||
|
my %counter = (
|
||||||
|
"processed" => 0,
|
||||||
|
"redisrst" => 0
|
||||||
|
);
|
||||||
|
my @RRType = ( "A", "AAAA", "CNAME", "NS" );
|
||||||
|
my %RRTypevalue = (
|
||||||
|
"A" => 1,
|
||||||
|
"AAAA" => 28,
|
||||||
|
"CNAME" => 5,
|
||||||
|
"NS" => 2,
|
||||||
|
"MX" => 15,
|
||||||
|
"SRV" => 33
|
||||||
|
);
|
||||||
|
|
||||||
|
my $parsedate = new Date::Manip::Date;
|
||||||
|
|
||||||
|
my $r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
|
||||||
|
|
||||||
|
my $stdin = new IO::Handle;
|
||||||
|
$stdin->fdopen( fileno(STDIN), "r" );
|
||||||
|
|
||||||
|
while ( defined( $_ = $stdin->getline ) ) {
|
||||||
|
|
||||||
|
if ( $counter{'redisrst'} == 1 ) {
|
||||||
|
$r->quit;
|
||||||
|
undef($r);
|
||||||
|
print "Reset\n";
|
||||||
|
$r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
|
||||||
|
$counter{'redisrst'} = 0;
|
||||||
|
}
|
||||||
|
if (m/^\[/) {
|
||||||
|
$block = 1;
|
||||||
|
{
|
||||||
|
my @s = split;
|
||||||
|
$date = $s[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !(m/^\[/) ) {
|
||||||
|
$_ =~ s/\cI//;
|
||||||
|
|
||||||
|
# discarding - [1.2.3.4].53 [149.13.33.69].5234
|
||||||
|
if (m/^\[/) { next; }
|
||||||
|
|
||||||
|
# split - 21 ns10.ovh.net,IN,AAAA,172800,2001:41d0:1:1981::1 \
|
||||||
|
if (m/^\d* /) {
|
||||||
|
$_ =~ s/^\d* //;
|
||||||
|
}
|
||||||
|
|
||||||
|
# discarding - dns QUERY,NOERROR,26278,qr|rd|ra \
|
||||||
|
if (m/^dns /) { next; }
|
||||||
|
|
||||||
|
# decode line
|
||||||
|
ProcessRequest( $date, $_ );
|
||||||
|
$counter{'processed'}++;
|
||||||
|
}
|
||||||
|
if ( ( $counter{'processed'} % 1000 ) == 0 ) {
|
||||||
|
$counter{'redisrst'} = 1;
|
||||||
|
print "Processed:"
|
||||||
|
. $counter{'processed'}
|
||||||
|
. " Redist RST:"
|
||||||
|
. $counter{'redisrst'} . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub ProcessRequest {
|
||||||
|
my $epoch = shift;
|
||||||
|
my $line = shift;
|
||||||
|
chomp($line);
|
||||||
|
|
||||||
|
#my $err = $parsedate->parse($date);
|
||||||
|
#my $epoch = $parsedate->printf('%s');
|
||||||
|
|
||||||
|
my @l = split( /,/, $line );
|
||||||
|
if ( $l[2] ~~ @RRType ) {
|
||||||
|
my @rdatas = split( / /, $l[4] );
|
||||||
|
|
||||||
|
#$l[4] =~ s/\s\\.*$//g;
|
||||||
|
#$l[4] =~ s/(.*)\s/$1/g;
|
||||||
|
print $l[0] . " " . $l[2] . " " . "$rdatas[0]\n";
|
||||||
|
my @rdatap = split( / /, $l[4] );
|
||||||
|
RedisUpdate( $l[0], $l[2], $rdatap[0], $epoch );
|
||||||
|
undef(@rdatap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub RedisUpdate {
|
||||||
|
my $name = lc(shift);
|
||||||
|
my $recordtype = shift;
|
||||||
|
my $rdata = lc(shift);
|
||||||
|
my $timestamp = shift;
|
||||||
|
|
||||||
|
my $recordtypevalue = $RRTypevalue{$recordtype};
|
||||||
|
my $pdns_r = "r:" . $name . ":" . $recordtypevalue;
|
||||||
|
my $pdns_v = "v:" . $rdata;
|
||||||
|
my $pdns_s = "s:" . $name . ":" . $rdata;
|
||||||
|
my $pdns_l = "l:" . $name . ":" . $rdata;
|
||||||
|
my $pdns_o = "o:" . $name . ":" . $rdata;
|
||||||
|
|
||||||
|
my $ret = $r->sadd( $pdns_r, $rdata );
|
||||||
|
$ret = $r->sadd( $pdns_v, $name );
|
||||||
|
|
||||||
|
# set first seen value
|
||||||
|
if ( !( $r->exists($pdns_s) ) ) {
|
||||||
|
$ret = $r->set( $pdns_s, $timestamp );
|
||||||
|
}
|
||||||
|
|
||||||
|
# set last seen value
|
||||||
|
$ret = $r->set( $pdns_l, $timestamp );
|
||||||
|
|
||||||
|
# increment the occurence value
|
||||||
|
$ret = $r->incr($pdns_o);
|
||||||
|
}
|
85
pdns-server/bin/pdns-ranking.py
Normal file
85
pdns-server/bin/pdns-ranking.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
#
|
||||||
|
# Minimal and Scalable Passive DNS - ranking records
|
||||||
|
#
|
||||||
|
# Copyright (C) 2012 Alexandre Dulaunoy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# Sample input (pdns-ranking.py) read line with the Passive DNS format:
|
||||||
|
# s:truecsi.org:ns3.afraid.org. = 1327737190
|
||||||
|
#
|
||||||
|
# and output the following:
|
||||||
|
# truecsi.org
|
||||||
|
# [(1.0003765060241001, 'truecsi.org')]
|
||||||
|
# truecsi.org,1.00037650602
|
||||||
|
#
|
||||||
|
# and update the DB 6 database
|
||||||
|
# redis> select 6
|
||||||
|
# OK
|
||||||
|
# redis> KEYS "*"
|
||||||
|
# 1) "truecsi.org"
|
||||||
|
# redis> GET "truecsi.org"
|
||||||
|
# "1.00037650602"
|
||||||
|
# redis>
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import redis
|
||||||
|
|
||||||
|
path = "../lib/DomainClassifier/DomainClassifier/"
|
||||||
|
sys.path.append(path)
|
||||||
|
|
||||||
|
import domainclassifier
|
||||||
|
|
||||||
|
|
||||||
|
def domexist(ldomain = None):
|
||||||
|
print ldomain
|
||||||
|
r = redis.StrictRedis(host='localhost', port=6379, db=6)
|
||||||
|
|
||||||
|
if ldomain is not None:
|
||||||
|
if r.exists(ldomain):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def domstore(ldomain = None, rank = 1.0):
|
||||||
|
r = redis.StrictRedis(host='localhost', port=6379, db=6)
|
||||||
|
return r.set(ldomain,rank)
|
||||||
|
|
||||||
|
for line in sys.stdin:
|
||||||
|
domain = line.split(':')[1].lower()
|
||||||
|
if not (re.search("(spamhaus.org|arpa)$", domain)):
|
||||||
|
if not domexist(ldomain=domain):
|
||||||
|
c = domainclassifier.Extract(domain)
|
||||||
|
c.domain()
|
||||||
|
c.validdomain()
|
||||||
|
r = c.rankdomain()
|
||||||
|
if r:
|
||||||
|
r.sort(reverse=True)
|
||||||
|
ranking = r[0][0]
|
||||||
|
print r
|
||||||
|
if ranking is not None:
|
||||||
|
print domain+","+str(ranking)
|
||||||
|
domstore(ldomain=domain, rank=ranking)
|
||||||
|
else:
|
||||||
|
print domain+","+str(1)
|
||||||
|
domstore(ldomain=domain)
|
||||||
|
else:
|
||||||
|
print domain+","+str(1)
|
||||||
|
domstore(ldomain=domain)
|
||||||
|
else:
|
||||||
|
print domain+" already cached"
|
114
pdns-server/bin/query.pl
Normal file
114
pdns-server/bin/query.pl
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
#
|
||||||
|
# Minimal and Scalable Passive DNS - Query tool
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010-2012 Alexandre Dulaunoy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
use POSIX qw(strftime);
|
||||||
|
use Redis;
|
||||||
|
use Date::Manip;
|
||||||
|
|
||||||
|
# will lookup the following RR types
|
||||||
|
my %RRTypevalue = (
|
||||||
|
"A" => 1,
|
||||||
|
"AAAA" => 28,
|
||||||
|
"CNAME" => 5,
|
||||||
|
"NS" => 2
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
my $r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
|
||||||
|
|
||||||
|
#sample feeder input
|
||||||
|
#1298823157.764221 "SET" "l:www.l.google.com:72.14.204.103" "1298819557"
|
||||||
|
#1298823157.764353 "INCR" "o:www.l.google.com:72.14.204.103"
|
||||||
|
#1298823157.765310 "SADD" "r:www.l.google.com:1" "72.14.204.104"
|
||||||
|
#1298823157.765439 "SADD" "v:72.14.204.104" "www.l.google.com"
|
||||||
|
#1298823157.765567 "EXISTS" "s:www.l.google.com:72.14.204.104"
|
||||||
|
#1298823157.765696 "SET" "l:www.l.google.com:72.14.204.104" "1298819557"
|
||||||
|
#1298823157.765823 "INCR" "o:www.l.google.com:72.14.204.104"
|
||||||
|
#1298823157.766776 "SADD" "r:www.l.google.com:1" "72.14.204.147"
|
||||||
|
#1298823157.766901 "SADD" "v:72.14.204.147" "www.l.google.com"
|
||||||
|
#1298823157.767086 "EXISTS" "s:www.l.google.com:72.14.204.147"
|
||||||
|
#1298823157.767207 "SET" "l:www.l.google.com:72.14.204.147" "1298819557"
|
||||||
|
#1298823157.767333 "INCR" "o:www.l.google.com:72.14.204.147"
|
||||||
|
|
||||||
|
while ( my ( $typename, $typevalue ) = each(%RRTypevalue) ) {
|
||||||
|
|
||||||
|
my @x = Lookup( $ARGV[0], $typevalue );
|
||||||
|
|
||||||
|
if ( defined(@x) ) {
|
||||||
|
foreach $p (@x) {
|
||||||
|
print $p. "("
|
||||||
|
. $typename . ")"
|
||||||
|
. " first seen: "
|
||||||
|
. NiceDate( FirstSeen( $ARGV[0], $p ) )
|
||||||
|
. " last seen: "
|
||||||
|
. NiceDate( LastSeen( $ARGV[0], $p ) )
|
||||||
|
. " Hits: "
|
||||||
|
. OccTuple( $ARGV[0], $p ) . "\n";
|
||||||
|
print " behind: " . join( ",", LookupIP($p) ) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub NiceDate {
|
||||||
|
my $epochvalue = shift;
|
||||||
|
|
||||||
|
return strftime( "%Y%m%d-%H:%M:%S", localtime($epochvalue) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub Lookup {
|
||||||
|
my $name = shift;
|
||||||
|
my $type = shift;
|
||||||
|
|
||||||
|
# default A record
|
||||||
|
if ( !( defined($type) ) ) { $type = 1; }
|
||||||
|
my $key = "r:$name:$type";
|
||||||
|
return $r->smembers($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub LookupIP {
|
||||||
|
my $name = shift;
|
||||||
|
my $key = "v:$name";
|
||||||
|
return $r->smembers($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub FirstSeen {
|
||||||
|
my $name = shift;
|
||||||
|
my $rdata = shift;
|
||||||
|
|
||||||
|
my $key = "s:$name:$rdata";
|
||||||
|
return $r->get($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub LastSeen {
|
||||||
|
my $name = shift;
|
||||||
|
my $rdata = shift;
|
||||||
|
|
||||||
|
my $key = "l:$name:$rdata";
|
||||||
|
return $r->get($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub OccTuple {
|
||||||
|
my $name = shift;
|
||||||
|
my $rdata = shift;
|
||||||
|
|
||||||
|
my $key = "o:$name:$rdata";
|
||||||
|
return $r->get($key);
|
||||||
|
}
|
79
pdns-server/doc/RR-Type.txt
Normal file
79
pdns-server/doc/RR-Type.txt
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
A 1 a host address [RFC1035]
|
||||||
|
NS 2 an authoritative name server [RFC1035]
|
||||||
|
MD 3 a mail destination (Obsolete - use MX) [RFC1035]
|
||||||
|
MF 4 a mail forwarder (Obsolete - use MX) [RFC1035]
|
||||||
|
CNAME 5 the canonical name for an alias [RFC1035]
|
||||||
|
SOA 6 marks the start of a zone of authority [RFC1035]
|
||||||
|
MB 7 a mailbox domain name (EXPERIMENTAL) [RFC1035]
|
||||||
|
MG 8 a mail group member (EXPERIMENTAL) [RFC1035]
|
||||||
|
MR 9 a mail rename domain name (EXPERIMENTAL) [RFC1035]
|
||||||
|
NULL 10 a null RR (EXPERIMENTAL) [RFC1035]
|
||||||
|
WKS 11 a well known service description [RFC1035]
|
||||||
|
PTR 12 a domain name pointer [RFC1035]
|
||||||
|
HINFO 13 host information [RFC1035]
|
||||||
|
MINFO 14 mailbox or mail list information [RFC1035]
|
||||||
|
MX 15 mail exchange [RFC1035]
|
||||||
|
TXT 16 text strings [RFC1035]
|
||||||
|
RP 17 for Responsible Person [RFC1183]
|
||||||
|
AFSDB 18 for AFS Data Base location [RFC1183][RFC5864]
|
||||||
|
X25 19 for X.25 PSDN address [RFC1183]
|
||||||
|
ISDN 20 for ISDN address [RFC1183]
|
||||||
|
RT 21 for Route Through [RFC1183]
|
||||||
|
NSAP 22 for NSAP address, NSAP style A record [RFC1706]
|
||||||
|
NSAP-PTR 23 for domain name pointer, NSAP style [RFC1348]
|
||||||
|
SIG 24 for security signature [RFC4034][RFC3755][RFC2535]
|
||||||
|
KEY 25 for security key [RFC4034][RFC3755][RFC2535]
|
||||||
|
PX 26 X.400 mail mapping information [RFC2163]
|
||||||
|
GPOS 27 Geographical Position [RFC1712]
|
||||||
|
AAAA 28 IP6 Address [RFC3596]
|
||||||
|
LOC 29 Location Information [RFC1876]
|
||||||
|
NXT 30 Next Domain - OBSOLETE [RFC3755][RFC2535]
|
||||||
|
EID 31 Endpoint Identifier [Patton]
|
||||||
|
NIMLOC 32 Nimrod Locator [Patton]
|
||||||
|
SRV 33 Server Selection [RFC2782]
|
||||||
|
ATMA 34 ATM Address [ATMDOC]
|
||||||
|
NAPTR 35 Naming Authority Pointer [RFC2915][RFC2168][RFC3403]
|
||||||
|
KX 36 Key Exchanger [RFC2230]
|
||||||
|
CERT 37 CERT [RFC4398]
|
||||||
|
A6 38 A6 (Experimental) [RFC3226][RFC2874]
|
||||||
|
DNAME 39 DNAME [RFC2672]
|
||||||
|
SINK 40 SINK [Eastlake]
|
||||||
|
OPT 41 OPT [RFC2671]
|
||||||
|
APL 42 APL [RFC3123]
|
||||||
|
DS 43 Delegation Signer [RFC4034][RFC3658]
|
||||||
|
SSHFP 44 SSH Key Fingerprint [RFC4255]
|
||||||
|
IPSECKEY 45 IPSECKEY [RFC4025]
|
||||||
|
RRSIG 46 RRSIG [RFC4034][RFC3755]
|
||||||
|
NSEC 47 NSEC [RFC4034][RFC3755]
|
||||||
|
DNSKEY 48 DNSKEY [RFC4034][RFC3755]
|
||||||
|
DHCID 49 DHCID [RFC4701]
|
||||||
|
NSEC3 50 NSEC3 [RFC5155]
|
||||||
|
NSEC3PARAM 51 NSEC3PARAM [RFC5155]
|
||||||
|
Unassigned 52-54
|
||||||
|
HIP 55 Host Identity Protocol [RFC5205]
|
||||||
|
NINFO 56 NINFO [Reid]
|
||||||
|
RKEY 57 RKEY [Reid]
|
||||||
|
TALINK 58 Trust Anchor LINK [Wijngaards]
|
||||||
|
Unassigned 59-98
|
||||||
|
SPF 99 [RFC4408]
|
||||||
|
UINFO 100 [IANA-Reserved]
|
||||||
|
UID 101 [IANA-Reserved]
|
||||||
|
GID 102 [IANA-Reserved]
|
||||||
|
UNSPEC 103 [IANA-Reserved]
|
||||||
|
Unassigned 104-248
|
||||||
|
TKEY 249 Transaction Key [RFC2930]
|
||||||
|
TSIG 250 Transaction Signature [RFC2845]
|
||||||
|
IXFR 251 incremental transfer [RFC1995]
|
||||||
|
AXFR 252 transfer of an entire zone [RFC1035][RFC5936]
|
||||||
|
MAILB 253 mailbox-related RRs (MB, MG or MR) [RFC1035]
|
||||||
|
MAILA 254 mail agent RRs (Obsolete - see MX) [RFC1035]
|
||||||
|
* 255 A request for all records [RFC1035]
|
||||||
|
URI 256 URI [Faltstrom]
|
||||||
|
Unassigned 257-32767
|
||||||
|
TA 32768 DNSSEC Trust Authorities [Weiler] 2005-12-13
|
||||||
|
DLV 32769 DNSSEC Lookaside Validation [RFC4431]
|
||||||
|
Unassigned 32770-65279
|
||||||
|
Private use 65280-65534
|
||||||
|
Reserved 65535
|
||||||
|
|
||||||
|
|
97
pdns-server/doc/datastore-format.txt
Normal file
97
pdns-server/doc/datastore-format.txt
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
This is a description of the various data-structure used
|
||||||
|
in the Passive DNS toolkit.
|
||||||
|
|
||||||
|
= Redis Database number =
|
||||||
|
|
||||||
|
- (0) default passive dns
|
||||||
|
- (6) ranking of hostname
|
||||||
|
|
||||||
|
= format for redis db 0 =
|
||||||
|
|
||||||
|
KEY, VALUE
|
||||||
|
|
||||||
|
s:<tuple> <epoch timestamp>
|
||||||
|
l:<tuple> <epoch timestamp>
|
||||||
|
o:<tuple> <integer value>
|
||||||
|
|
||||||
|
KEY, {SET}
|
||||||
|
|
||||||
|
r:<record>:<rr-type-value> {<answer>}
|
||||||
|
v:<answer> {<record>}
|
||||||
|
|
||||||
|
# "SET" "l:www.l.google.com:72.14.204.103" "1298819557"
|
||||||
|
#1298823157.764353 "INCR" "o:www.l.google.com:72.14.204.103"
|
||||||
|
#1298823157.765310 "SADD" "r:www.l.google.com:1" "72.14.204.104"
|
||||||
|
#1298823157.765439 "SADD" "v:72.14.204.104" "www.l.google.com"
|
||||||
|
#1298823157.765567 "EXISTS" "s:www.l.google.com:72.14.204.104"
|
||||||
|
#1298823157.765696 "SET" "l:www.l.google.com:72.14.204.104" "1298819557"
|
||||||
|
#1298823157.765823 "INCR" "o:www.l.google.com:72.14.204.104"
|
||||||
|
#1298823157.766776 "SADD" "r:www.l.google.com:1" "72.14.204.147"
|
||||||
|
#1298823157.766901 "SADD" "v:72.14.204.147" "www.l.google.com"
|
||||||
|
#1298823157.767086 "EXISTS" "s:www.l.google.com:72.14.204.147"
|
||||||
|
#1298823157.767207 "SET" "l:www.l.google.com:72.14.204.147" "1298819557"
|
||||||
|
#1298823157.767333 "INCR" "o:www.l.google.com:72.14.204.147"
|
||||||
|
|
||||||
|
|
||||||
|
== sample db 0 ==
|
||||||
|
|
||||||
|
Here is the classical update process for a feeder:
|
||||||
|
|
||||||
|
Set or update the last-seen.
|
||||||
|
|
||||||
|
#1298823157.764221 "SET" "l:www.l.google.com:72.14.204.103" "1298819557"
|
||||||
|
|
||||||
|
Update the occurence of the answer seen.
|
||||||
|
|
||||||
|
#1298823157.764353 "INCR" "o:www.l.google.com:72.14.204.103"
|
||||||
|
|
||||||
|
Add to a set "record:rr-type" the seen answer.
|
||||||
|
|
||||||
|
#1298823157.765310 "SADD" "r:www.l.google.com:1" "72.14.204.104"
|
||||||
|
|
||||||
|
Add the reverse for fast lookup.
|
||||||
|
|
||||||
|
#1298823157.765439 "SADD" "v:72.14.204.104" "www.l.google.com"
|
||||||
|
|
||||||
|
If there is no existing first-seen for the tuple,
|
||||||
|
|
||||||
|
#1298823157.765567 "EXISTS" "s:www.l.google.com:72.14.204.104"
|
||||||
|
|
||||||
|
A first-seen is set:
|
||||||
|
|
||||||
|
#1298823157.765696 "SET" "l:www.l.google.com:72.14.204.104" "1298819557"
|
||||||
|
|
||||||
|
and so on...
|
||||||
|
|
||||||
|
#1298823157.765823 "INCR" "o:www.l.google.com:72.14.204.104"
|
||||||
|
#1298823157.766776 "SADD" "r:www.l.google.com:1" "72.14.204.147"
|
||||||
|
#1298823157.766901 "SADD" "v:72.14.204.147" "www.l.google.com"
|
||||||
|
#1298823157.767086 "EXISTS" "s:www.l.google.com:72.14.204.147"
|
||||||
|
#1298823157.767207 "SET" "l:www.l.google.com:72.14.204.147" "1298819557"
|
||||||
|
#1298823157.767333 "INCR" "o:www.l.google.com:72.14.204.147"
|
||||||
|
|
||||||
|
= format for redis db 6 =
|
||||||
|
|
||||||
|
This db index hold a ranking value per hostname.
|
||||||
|
|
||||||
|
KEY, VALUE
|
||||||
|
|
||||||
|
hostname, float double precision
|
||||||
|
|
||||||
|
This can be seen as a cache when an expiration can be set
|
||||||
|
by key to limit the lifetime of a ranking. You can create
|
||||||
|
a persistent ranking for setting up some "golden" domains.
|
||||||
|
|
||||||
|
== sample db 6 ==
|
||||||
|
|
||||||
|
1) "truecsi.org"
|
||||||
|
redis> GET "truecsi.org"
|
||||||
|
"1.00037650602"
|
||||||
|
redis>
|
||||||
|
|
||||||
|
= contact =
|
||||||
|
|
||||||
|
Update, extension, or new data-structure more than welcome.
|
||||||
|
|
||||||
|
https://www.github.com/adulau/
|
||||||
|
|
3
pdns-server/lib/install.sh
Normal file
3
pdns-server/lib/install.sh
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
git clone git://github.com/adulau/DomainClassifier.git
|
171
pdns-server/web/PDNSq.pm
Normal file
171
pdns-server/web/PDNSq.pm
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
#
|
||||||
|
# Minimal and Scalable Passive DNS - Query tool
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010-2012 Alexandre Dulaunoy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package PDNS::Query;
|
||||||
|
|
||||||
|
use base 'Exporter';
|
||||||
|
|
||||||
|
use POSIX qw(strftime);
|
||||||
|
use Redis;
|
||||||
|
use Date::Manip;
|
||||||
|
use Time::Format;
|
||||||
|
|
||||||
|
my %RRTypevalue = (
|
||||||
|
"A" => 1,
|
||||||
|
"NS" => 2,
|
||||||
|
"AAAA" => 28,
|
||||||
|
"CNAME" => 5
|
||||||
|
);
|
||||||
|
|
||||||
|
my $r = Redis->new( server => '127.0.0.1:6379', encoding => undef );
|
||||||
|
|
||||||
|
my $burl = "/l/";
|
||||||
|
|
||||||
|
sub Query {
|
||||||
|
|
||||||
|
my $query = shift;
|
||||||
|
my $ret = "";
|
||||||
|
$ret .= "<a id=\"v_toggle\" href=\"\#\">[+-]</a>";
|
||||||
|
$ret .= "<div id=\"vertical_slide\">";
|
||||||
|
|
||||||
|
while ( my ( $typename, $typevalue ) = each(%RRTypevalue) ) {
|
||||||
|
|
||||||
|
my @x = Lookup( $query, $typevalue );
|
||||||
|
if ( defined(@x) ) {
|
||||||
|
foreach $p (@x) {
|
||||||
|
if ( $typename =~ m/CNAME/ ) {
|
||||||
|
$ret .= MakeLink($p) . "(" . $typename . ")";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$ret .= $p . "(" . $typename . ")";
|
||||||
|
}
|
||||||
|
$ret .=
|
||||||
|
" first seen: "
|
||||||
|
. NiceDate( FirstSeen( $query, $p ) )
|
||||||
|
. " last seen: "
|
||||||
|
. NiceDate( LastSeen( $query, $p ) )
|
||||||
|
. " Hits: "
|
||||||
|
. OccTuple( $query, $p ) . "\n";
|
||||||
|
|
||||||
|
#$ret .= " behind: " . join( "" , LookupIP($p) ) . "\n";
|
||||||
|
$ret .= "<br />";
|
||||||
|
foreach $n ( LookupIP($p) ) {
|
||||||
|
$ret .= MakeLink($n);
|
||||||
|
}
|
||||||
|
$ret .= "<br /> <br />";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$ret .= "</div>";
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub QueryJSON {
|
||||||
|
my $query = shift;
|
||||||
|
my $ret = <<JSHEAD;
|
||||||
|
var event_data =
|
||||||
|
{
|
||||||
|
"dateTimeFormat": "iso8601",
|
||||||
|
"events":[
|
||||||
|
JSHEAD
|
||||||
|
while ( my ( $typename, $typevalue ) = each(%RRTypevalue) ) {
|
||||||
|
my @x = Lookup( $query, $typevalue );
|
||||||
|
|
||||||
|
if ( defined(@x) ) {
|
||||||
|
foreach $p (@x) {
|
||||||
|
$ret .= "\{\"start\": \""
|
||||||
|
. NiceDateISO( FirstSeen( $query, $p ) ) . "\",\n";
|
||||||
|
$ret .= "\"end\": \""
|
||||||
|
. NiceDateISO( LastSeen( $query, $p ) ) . "\",\n";
|
||||||
|
chomp($p);
|
||||||
|
$ret .= "\"title\": \"" . $p . "(" . $typename . ")\",\n";
|
||||||
|
$ret .= "\"description\": \"" . $p . "(" . $typename . ")\",\n";
|
||||||
|
$ret .= "\"instant\": \"false\",\n";
|
||||||
|
$ret .= "\},\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
chop($ret);
|
||||||
|
chop($ret);
|
||||||
|
return $ret . "] };\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
sub MakeLink {
|
||||||
|
my $value = shift;
|
||||||
|
my $ret = "<a href=\"" . $burl . "" . $value . "\">$value</a> ";
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub NiceDate {
|
||||||
|
my $epochvalue = shift;
|
||||||
|
|
||||||
|
return strftime( "%Y%m%d-%H:%M:%S", localtime($epochvalue) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub NiceDateISO {
|
||||||
|
my $epochvalue = shift;
|
||||||
|
my $format = "yyyy-mm{on}-ddThh:mm:ss+00:00";
|
||||||
|
return time_format( $format, $epochvalue );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub Lookup {
|
||||||
|
my $name = shift;
|
||||||
|
my $type = shift;
|
||||||
|
|
||||||
|
# default A record
|
||||||
|
if ( !( defined($type) ) ) { $type = 1; }
|
||||||
|
my $key = "r:$name:$type";
|
||||||
|
return $r->smembers($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub LookupIP {
|
||||||
|
my $name = shift;
|
||||||
|
my $key = "v:$name";
|
||||||
|
return $r->smembers($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub FirstSeen {
|
||||||
|
my $name = shift;
|
||||||
|
my $rdata = shift;
|
||||||
|
|
||||||
|
my $key = "s:$name:$rdata";
|
||||||
|
return $r->get($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub LastSeen {
|
||||||
|
my $name = shift;
|
||||||
|
my $rdata = shift;
|
||||||
|
|
||||||
|
my $key = "l:$name:$rdata";
|
||||||
|
return $r->get($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub OccTuple {
|
||||||
|
my $name = shift;
|
||||||
|
my $rdata = shift;
|
||||||
|
|
||||||
|
my $key = "o:$name:$rdata";
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
#return $r->get($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
214
pdns-server/web/pdns-web.pl
Executable file
214
pdns-server/web/pdns-web.pl
Executable file
|
@ -0,0 +1,214 @@
|
||||||
|
#!/bin/env perl
|
||||||
|
#
|
||||||
|
# Minimal and Scalable Passive DNS - Web interface
|
||||||
|
#
|
||||||
|
# Copyright (C) 2010-2012 Alexandre Dulaunoy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
use Mojolicious::Lite;
|
||||||
|
use Mojolicious::Static;
|
||||||
|
use PDNSq;
|
||||||
|
|
||||||
|
# Documentation browser under "/perldoc" (this plugin requires Perl 5.10)
|
||||||
|
plugin 'pod_renderer';
|
||||||
|
|
||||||
|
get '/l/(.query)' => sub {
|
||||||
|
my $self = shift;
|
||||||
|
my $q = $self->param('query');
|
||||||
|
my $ret = PDNS::Query::Query($q);
|
||||||
|
my $json = PDNS::Query::QueryJSON($q);
|
||||||
|
$self->render(template => 'index', q => $ret, query => $q, jq => $json);
|
||||||
|
};
|
||||||
|
|
||||||
|
post '/l' => sub {
|
||||||
|
my $self = shift;
|
||||||
|
my $q = $self->param('q');
|
||||||
|
my $ret = PDNS::Query::Query($q);
|
||||||
|
my $json = PDNS::Query::QueryJSON($q);
|
||||||
|
$self->render(template => 'index', q => $ret, query => $q, jq => $json);
|
||||||
|
};
|
||||||
|
|
||||||
|
get '/js/(.query)' => sub {
|
||||||
|
my $self = shift;
|
||||||
|
my $q = $self->param('query');
|
||||||
|
$self->render_static('static/'.$q);
|
||||||
|
};
|
||||||
|
|
||||||
|
get '/__history__.html' => sub {
|
||||||
|
my $self = shift;
|
||||||
|
$self->render_static('static/__history__.html');
|
||||||
|
};
|
||||||
|
|
||||||
|
get '/' => sub {
|
||||||
|
my $self = shift;
|
||||||
|
$self->render(template => 'main');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
app->secret('You have to change it');
|
||||||
|
app->start;
|
||||||
|
__DATA__
|
||||||
|
|
||||||
|
@@ main.html.ep
|
||||||
|
% layout 'default';
|
||||||
|
% title 'Passive DNS';
|
||||||
|
<h1>Passive DNS interface</h1>
|
||||||
|
<form name="lookup" action="/l/"i method=POST>
|
||||||
|
<input type="text" name="q" />
|
||||||
|
<input type="submit" value="lookup"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@@ index.html.ep
|
||||||
|
% layout 'default';
|
||||||
|
% title 'PDNS Lookup';
|
||||||
|
<h1>Passive DNS lookup result for <a href="/l/<%= $query %>"><%= $query %></a></h1>
|
||||||
|
<a href="#visual">(go to visual timeline)</a><br /><br />
|
||||||
|
<%== $q %>
|
||||||
|
<a name="visual">Visual timeline</a>
|
||||||
|
<div id="tl" class="timeline-default" style="height: 400px; border: 1px solid #aaa;"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
This page uses Javascript to show you a Timeline. Please enable Javascript in your browser to see the full page. Thank you.
|
||||||
|
</noscript>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var tl;
|
||||||
|
//in php you can get this 1so8601 date using date("c",$you_date_variable);
|
||||||
|
var startProj = SimileAjax.DateTime.parseIso8601DateTime("2011-03-01T00:00:00");
|
||||||
|
var endProj = SimileAjax.DateTime.parseIso8601DateTime("2011-12-30T00:00:00");
|
||||||
|
|
||||||
|
<%== $jq %>
|
||||||
|
|
||||||
|
function onLoad() {
|
||||||
|
var tl_el = document.getElementById("tl");
|
||||||
|
var eventSource = new Timeline.DefaultEventSource();
|
||||||
|
var theme = Timeline.ClassicTheme.create();
|
||||||
|
theme.autoWidth = true; // Set the Timeline's "width" automatically.
|
||||||
|
theme.autoWidthMargin=10;
|
||||||
|
theme.event.bubble.width = 220;
|
||||||
|
theme.event.bubble.height = 120;
|
||||||
|
|
||||||
|
theme.ether.backgroundColors = ["#E6E6E6","#F7F7F7"];
|
||||||
|
|
||||||
|
theme.timeline_start = startProj;
|
||||||
|
theme.timeline_stop = endProj;
|
||||||
|
|
||||||
|
theme.event.track.height = "20";
|
||||||
|
theme.event.tape.height = 10; // px
|
||||||
|
theme.event.track.height = theme.event.tape.height + 6;
|
||||||
|
|
||||||
|
var d = SimileAjax.DateTime.parseIso8601DateTime("2011-03-01T00:00:00");
|
||||||
|
var bandInfos = [
|
||||||
|
|
||||||
|
Timeline.createBandInfo({
|
||||||
|
layout: 'original',// original, overview, detailed
|
||||||
|
eventSource: eventSource,
|
||||||
|
date: d,
|
||||||
|
width: 350,
|
||||||
|
intervalUnit: Timeline.DateTime.DAY,
|
||||||
|
intervalPixels: 100,
|
||||||
|
//trackHeight: 10,
|
||||||
|
theme :theme
|
||||||
|
|
||||||
|
}),
|
||||||
|
Timeline.createBandInfo({
|
||||||
|
layout: 'overview',
|
||||||
|
date: d,
|
||||||
|
trackHeight: 0.5,
|
||||||
|
trackGap: 0.2,
|
||||||
|
eventSource: eventSource,
|
||||||
|
width: 50,
|
||||||
|
intervalUnit: Timeline.DateTime.MONTH,
|
||||||
|
// showEventText: false,
|
||||||
|
intervalPixels: 200,
|
||||||
|
theme :theme
|
||||||
|
})
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
bandInfos[1].highlight = true;
|
||||||
|
bandInfos[1].syncWith = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bandInfos[1].decorators = [
|
||||||
|
new Timeline.SpanHighlightDecorator({
|
||||||
|
// startDate: startProj,
|
||||||
|
// endDate: endProj,
|
||||||
|
inFront: false,
|
||||||
|
color: "#FFC080",
|
||||||
|
opacity: 30,
|
||||||
|
startLabel: "Begin",
|
||||||
|
endLabel: "End",
|
||||||
|
theme: theme
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
tl = Timeline.create(tl_el, bandInfos, Timeline.HORIZONTAL);
|
||||||
|
// show loading message
|
||||||
|
tl.showLoadingMessage();
|
||||||
|
|
||||||
|
eventSource.loadJSON(event_data, document.location.href);
|
||||||
|
|
||||||
|
// dismiss loading message
|
||||||
|
tl.hideLoadingMessage();
|
||||||
|
|
||||||
|
// setup highlight filters
|
||||||
|
//setupFilterHighlightControls(document.getElementById("controls"), tl, [0,1], theme);
|
||||||
|
|
||||||
|
}
|
||||||
|
//function centerProjStart() {
|
||||||
|
// tl.getBand(1).setCenterVisibleDate(startProj);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//function centerProjEnd() {
|
||||||
|
// tl.getBand(1).setCenterVisibleDate(endProj);
|
||||||
|
// }
|
||||||
|
|
||||||
|
var resizeTimerID = null;
|
||||||
|
function onResize() {
|
||||||
|
if(resizeTimerID == null) {
|
||||||
|
resizeTimerID = window.setTimeout(function() {
|
||||||
|
resizeTimerID = null;
|
||||||
|
tl.layout();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@@ layouts/default.html.ep
|
||||||
|
<!doctype html><html>
|
||||||
|
<head><title><%= title %></title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="http://www.circl.lu/css/styles.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/js/pdns.css">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||||
|
<script src="http://static.simile.mit.edu/timeline/api-2.3.0/timeline-api.js?bundle=true" type="text/javascript"></script>
|
||||||
|
<script src="/js/mootools-core-1.3.1.js" type="text/javascript"></script>
|
||||||
|
<script src="/js/mootools-more-1.3.1.1.js" type="text/javascript"></script>
|
||||||
|
<script src="/js/moo-pdns.js" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body onload="onLoad();" onresize="onResize();">
|
||||||
|
<div class="header" id="header">Passive DNS toolkit
|
||||||
|
</div>
|
||||||
|
<div class="body" id="body">
|
||||||
|
<%= content =%></body>
|
||||||
|
</div>
|
||||||
|
</html>
|
5
pdns-server/web/pdns-web.sh
Normal file
5
pdns-server/web/pdns-web.sh
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Will listen on port 3000
|
||||||
|
perl pdns-web.pl daemon --reload
|
||||||
|
|
17
pdns-server/web/public/static/moo-pdns.js
Normal file
17
pdns-server/web/public/static/moo-pdns.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
window.addEvent('domready', function() {
|
||||||
|
|
||||||
|
var status = {
|
||||||
|
'true': 'open',
|
||||||
|
'false': 'close'
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- vertical
|
||||||
|
|
||||||
|
var myVerticalSlide = new Fx.Slide('vertical_slide');
|
||||||
|
|
||||||
|
document.id('v_toggle').addEvent('click', function(event){
|
||||||
|
event.stop();
|
||||||
|
myVerticalSlide.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
5928
pdns-server/web/public/static/mootools-core-1.3.1-full-compat.js
vendored
Normal file
5928
pdns-server/web/public/static/mootools-core-1.3.1-full-compat.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
5928
pdns-server/web/public/static/mootools-core-1.3.1.js
vendored
Normal file
5928
pdns-server/web/public/static/mootools-core-1.3.1.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
13463
pdns-server/web/public/static/mootools-more-1.3.1.1.js
Normal file
13463
pdns-server/web/public/static/mootools-more-1.3.1.1.js
Normal file
File diff suppressed because it is too large
Load diff
16
pdns-server/web/public/static/pdns.css
Normal file
16
pdns-server/web/public/static/pdns.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
h3.section {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#vertical_slide, #horizontal_slide {
|
||||||
|
background: #D0C8C8;
|
||||||
|
color: #8A7575;
|
||||||
|
padding: 10px;
|
||||||
|
border: 5px solid #F3F1F1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.marginbottom {
|
||||||
|
/* Since the Fx.Slide element resets margins, we set a margin on the above element */
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
Loading…
Reference in a new issue