Friday, January 07, 2011

xmllint - Answer to an XML Question

Today I was asked about how I can validate or check the well-formness of XML file. My immediate answers were using a browser to view the malformed XML and the second answer was to parse it using tdom. At that time, xmllint wasn't in my mind. 'cos I seldom use it.

After some thoughts, I think I should validate my anwser. I downloaded a pretty sizeable XML file from Mondial project for my test. I deliberately removed one of the closing tags to make it not well-formed. Both tdom and Firefox are not able to identify the exact location of the missing closing tag. It is only xmllint is able to pinpoint the location

$ diff mondial.xml mondial-malformed.xml 
16819d16818
<    </country>


$ firefox mondial-malformed.xml

Firefox
XML Parsing Error: mismatched tag. Expected: </country>.
Location: file:///home/chihung/Projects/xmllint/mondial-malformed.xml
Line Number 39564, Column 3:</mondial>
--^


$ tclsh
% package require tdom
0.8.3
% set doc [dom parse [tDOM::xmlReadFile mondial-malformed.xml]]
error "mismatched tag" at line 39564 character 2
"ude>
   </desert>
</m <--Error-- ondial>
"


$ xmllint --shell mondial-malformed.xml 
mondial-malformed.xml:39564: parser error : Opening and ending tag mismatch: country line 16795 and mondial
</mondial>
          ^
mondial-malformed.xml:39565: parser error : Premature end of data in tag mondial line 3

^

OK, xmllint is sure the winner in this exercise. Below shows xmllint in action:

$ xmllint --shell mondial.xml 
/ > help
 base         display XML base of the node
 setbase URI  change the XML base of the node
 bye          leave shell
 cat [node]   display node or current node
 cd [path]    change directory to path or to root
 dir [path]   dumps informations about the node (namespace, attributes, content)
 du [path]    show the structure of the subtree under path or the current node
 exit         leave shell
 help         display this help
 free         display memory usage
 load [name]  load a new document with name
 ls [path]    list contents of path or the current directory
 set xml_fragment replace the current node content with the fragment parsed in context
 xpath expr   evaluate the XPath expression in that context and print the result
 setns nsreg  register a namespace to a prefix in the XPath evaluation context
              format for nsreg is: prefix=[nsuri] (i.e. prefix= unsets a prefix)
 setrootns    register all namespace found on the root element
              the default namespace if any uses 'defaultns' prefix
 pwd          display current working directory
 quit         leave shell
 save [name]  save this document to name or the original name
 write [name] write the current node to the filename
 validate     check the document for errors
 relaxng rng  validate the document agaisnt the Relax-NG schemas
 grep string  search for a string in the subtree

/ > validate
mondial.xml:35144: element island: validity error : Syntax of value for attribute sea of island is not valid
validity error : attribute sea line 35144 references an unknown ID ""

/ > base
mondial.xml

/ > dir
DOCUMENT
version=1.0
encoding=UTF-8
URL=mondial.xml
standalone=true

/ > grep Singapore
/mondial/country[105]/name : t--        9 Singapore
/mondial/country[105]/city/name : t--        9 Singapore
/mondial/island[163]/name : t--        9 Singapore

/ > cd /mondial/country[105]

country > cat
<country car_code="SGP" area="632.6" capital="cty-Singapore-Singapore" memberships="org-AsDB org-ASEAN org-Mekong-Group org-CP org-C org-CCC org-ESCAP org-G-77 org-IAEA org-IBRD org-ICC org-ICAO org-ICFTU org-Interpol org-IFRCS org-IFC org-ILO org-IMO org-Inmarsat org-IMF org-IOC org-ISO org-ICRM org-ITU org-Intelsat org-NAM org-PCA org-UN org-UNIKOM org-UPU org-WHO org-WIPO org-WMO org-WTrO">
      <name>Singapore</name>
      <population>3396924</population>
      <population_growth>1.9</population_growth>
      <infant_mortality>4.7</infant_mortality>
      <gdp_total>66100</gdp_total>
      <gdp_ind>28</gdp_ind>
      <gdp_serv>72</gdp_serv>
      <inflation>1.7</inflation>
      <indep_date>1965-08-09</indep_date>
      <government>republic within Commonwealth</government>
      <encompassed continent="asia" percentage="100"/>
      <ethnicgroups percentage="6.4">Indian</ethnicgroups>
      <ethnicgroups percentage="76.4">Chinese</ethnicgroups>
      <ethnicgroups percentage="14.9">Malay</ethnicgroups>
      <city id="cty-Singapore-Singapore" is_country_cap="yes" country="SGP">
         <name>Singapore</name>
         <longitude>103.833</longitude>
         <latitude>1.3</latitude>
         <population year="87">2558000</population>
         <located_at watertype="sea" sea="sea-SouthChinaSea"/>
         <located_on island="island-Singapore"/>
      </city>
   </country>

Finding countries with infant_mortality less than Singapore.

country > xpath //country[infant_mortality<4.7]/name/text()
Object is a Node Set :
Set contains 9 nodes:
1  TEXT
    content=Andorra
2  TEXT
    content=Sweden
3  TEXT
    content=Iceland
4  TEXT
    content=Jersey
5  TEXT
    content=Man
6  TEXT
    content=Hong Kong
7  TEXT
    content=Japan
8  TEXT
    content=Anguilla
9  TEXT
    content=Bermuda

country > quit

This can be turned into command line too.

$ xmllint --xpath '//country[infant_mortality<4.7]/name' --format mondial.xml 
<name>Andorra</name><name>Sweden</name><name>Iceland</name><name>Jersey</name><name>Man</name><name>Hong Kong</name><name>Japan</name><name>Anguilla</name><name>Bermuda</name>

real 0m0.219s
user 0m0.192s
sys 0m0.020s

Alternatively, you can do the above dynamically:

$ xmllint --shell mondial.xml
/ > xpath //country[infant_mortality<//country[name="Singapore"]/infant_mortality]/name/text()
Object is a Node Set :
Set contains 9 nodes:
1  TEXT
    content=Andorra
2  TEXT
    conte;nt=Sweden
3  TEXT
    content=Iceland
4  TEXT
    content=Jersey
5  TEXT
    content=Man
6  TEXT
    content=Hong Kong
7  TEXT
    content=Japan
8  TEXT
    content=Anguilla
9  TEXT
    content=Bermuda

$ time xmllint --xpath '//country[infant_mortality<//country[name="Singapore"]/infant_mortality]/name' --format mondial.xml 
<name>Andorra</name><name>Sweden</name><name>Iceland</name><name>Jersey</name><name>Man</name><name>Hong Kong</name><name>Japan</name><name>Anguilla</name><name>Bermuda</name>
real 0m2.074s
user 0m2.052s
sys 0m0.016s

xmllint is definitely the preferred XML companion. It is extremely fast and efficient comparing with Firefox and tdom.

Labels: ,

Thursday, June 26, 2008

I Knew What You Did In Your Sessions

This blog title sounds like the 1997 movie I Knew What You Did Last Summer. Yes indeed, but in Solaris operating system, not in cinema. By turning on the auditing feature in Solaris, you can really tell what the user did in all his/her sessions. However the default setting in audit_control does not capture the execve system call. In order to find out the command executed together with the argument supplied to it, you need to include the "ex" in the flags: in /etc/security/audit_control and include "/usr/sbin/auditconfig -setpolicy +argv" in /etc/security/audit_startup This is what I have in my demo server:
# egrep -v '^#' /etc/security/audit_control
dir:/var/audit
flags:lo,fc,fm,fd,ex,xp,xc,xs,xx,fa
minfree:20
naflags:lo

# egrep -v '^#' /etc/security/audit_startup
/usr/bin/echo "Starting BSM services."
/usr/sbin/auditconfig -setpolicy +cnt
/usr/sbin/auditconfig -setpolicy +arg
/usr/sbin/auditconfig -conf
/usr/sbin/auditconfig -aconf

Once that has been configured properly, you can start the auditing by rebooting the server.

# cd /etc/security

# ./bsmconv
This script is used to enable the Basic Security Module (BSM).
Shall we continue with the conversion now? [y/n] y
bsmconv : INFO : checking startup file .
bsmconv : INFO : turning on audit module .
bsmconv : INFO : initializing device allocation .
The Basic Security Module is ready .
If there were any errors , please fix them now .
Configure BSM by editing files located in /etc/ security .
Reboot this system now to come up with BSM enabled .

# reboot


----- wait for the system to boot up -----



# svcs | grep "auditd"
online 23:30:03 svc:/system/auditd:default

The default location for the audit trail is in /var/audit. It is best practice to ensure you have sufficient space in this directory and /var (or /var/audit) is a separate partition because the audit trail file grows very fast in a production system. The audit trail files are stored in binary format and the data structure is described in audit.log. Two built-in utilities are provided in Solaris to streamline the audit reporting, they are auditreduce and praudit. The praudit default output is pretty hard to understand and comma separated variable output is not in fix-field format

# auditreduce | praudit | head
file,2008-06-23 10:56:27.000 +08:00,
header,44,2,system booted,na,2008-06-23 10:56:27.330 +08:00
text,booting kernel
header,145,2,open(2) - read,,solaris11,2008-06-23 11:02:51.437 +08:00
path,/devices/pseudo/pool@0:pool
attribute,20666,root,sys,336,118489094,970662608897
subject,chihung,root,root,root,root,674,2971128068,787 65558 10.1.2.84
return,success,7
header,120,2,open(2) - read,write,sp,solaris11,2008-06-23 11:02:51.467 +08:00
path,/var/adm/lastlog

However, if you read the man page of praudit, you will realise that it can dump the audit trail in XML. Here is a sample dump:

# auditreduce | praudit -x | head -50
<?xml version='1.0' encoding='UTF-8' ?>
<?xml-stylesheet type='text/xsl' href='file:///usr/share/lib/xml/style/adt_record.xsl.1' ?>

<!DOCTYPE audit PUBLIC '-//Sun Microsystems, Inc.//DTD Audit V1//EN' 'file:///usr/share/lib/xml/dtd/adt_record.dtd.1'>

<audit>
<file iso8601="2008-06-24 13:14:44.000 +08:00"></file>
<record version="2" event="fcntl(2)" host="solaris11" iso8601="2008-06-24 13:14:44.536 +08:00">
<argument arg-num="2" value="0x4" desc="cmd"/>
<argument arg-num="1" value="0x6" desc="no path: fd"/>
<attribute mode="10000" uid="root" gid="root" fsid="344" nodeid="323" device="0"/>
<subject audit-uid="chihung" uid="root" gid="root" ruid="root" rgid="root" pid="704" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="0"/>
</record>
<record version="2" event="fcntl(2)" host="solaris11" iso8601="2008-06-24 13:14:44.536 +08:00">
<argument arg-num="2" value="0x4" desc="cmd"/>
<argument arg-num="1" value="0x7" desc="no path: fd"/>
<attribute mode="10000" uid="root" gid="root" fsid="344" nodeid="323" device="0"/>
<subject audit-uid="chihung" uid="root" gid="root" ruid="root" rgid="root" pid="707" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="0"/>
</record>
<record version="2" event="fcntl(2)" host="solaris11" iso8601="2008-06-24 13:14:44.542 +08:00">
<argument arg-num="2" value="0x3" desc="cmd"/>
<argument arg-num="1" value="0x5" desc="no path: fd"/>
<attribute mode="10000" uid="chihung" gid="other" fsid="344" nodeid="324" device="0"/>
<subject audit-uid="chihung" uid="chihung" gid="other" ruid="chihung" rgid="other" pid="707" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="2"/>
</record>
<record version="2" event="fcntl(2)" host="solaris11" iso8601="2008-06-24 13:14:44.542 +08:00">
<argument arg-num="2" value="0x4" desc="cmd"/>
<argument arg-num="1" value="0x5" desc="no path: fd"/>
<attribute mode="10000" uid="chihung" gid="other" fsid="344" nodeid="324" device="0"/>
<subject audit-uid="chihung" uid="chihung" gid="other" ruid="chihung" rgid="other" pid="707" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="0"/>
</record>
<record version="2" event="fcntl(2)" host="solaris11" iso8601="2008-06-24 13:14:44.542 +08:00">
<argument arg-num="2" value="0x3" desc="cmd"/>
<argument arg-num="1" value="0x6" desc="no path: fd"/>
<attribute mode="10000" uid="chihung" gid="other" fsid="344" nodeid="324" device="0"/>
<subject audit-uid="chihung" uid="chihung" gid="other" ruid="chihung" rgid="other" pid="707" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="2"/>
</record>
<record version="2" event="fcntl(2)" host="solaris11" iso8601="2008-06-24 13:14:44.542 +08:00">
<argument arg-num="2" value="0x4" desc="cmd"/>
<argument arg-num="1" value="0x6" desc="no path: fd"/>
<attribute mode="10000" uid="chihung" gid="other" fsid="344" nodeid="324" device="0"/>
<subject audit-uid="chihung" uid="chihung" gid="other" ruid="chihung" rgid="other" pid="707" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="0"/>
</record>
<record version="2" event="access(2)" host="solaris11" iso8601="2008-06-24 13:14:44.547 +08:00">
<path>/dev/pts/1</path>
<attribute mode="20620" uid="chihung" gid="tty" fsid="337" nodeid="5942560" device="103079215105"/>
<subject audit-uid="chihung" uid="chihung" gid="other" ruid="chihung" rgid="other" pid="707" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="0"/>
</record>
<record version="2" event="stat(2)" host="solaris11" iso8601="2008-06-24 13:14:44.548 +08:00">
<path>/dev/pts/1</path>
<attribute mode="20620" uid="chihung" gid="tty" fsid="337" nodeid="5942560" device="103079215105"/>
<subject audit-uid="chihung" uid="chihung" gid="other" ruid="chihung" rgid="other" pid="707" sid="1932086420" tid="639 65558 10.1.2.84"/>
<return errval="success" retval="0"/>
</record>

"sid" is the session ID and it is not difficult to find out how many records for all the sessions using Tcl (with tDOM extension). Once we locate the session ID, we can dump out all the execve commands and there corresponding arguments. auditreduce is able merge and select audit records from audit trail files, -a and -b flags to dump audit records between two timestamps. See the man page for all other options to reduce the audit trail output before it is piped to praudit.

$ ./sid.tcl
Usage: ./sid.tcl <xmlfile> <user>

$ ./sid.tcl sample.xml chihung
3331057518=19
2156193989=19
2283295222=19
740162063=177
3244834577=22
1932086420=387

$ ./sid-exec.tcl
Usage: ./sid-exec.tcl <xmlfile> <sid>

$ ./sid-exec.tcl sample.xml 740162063
/usr/bin/bash  ; -bash  ; success
/usr/lib/fs/ufs/quota  ; /usr/sbin/quota  ; success
/usr/bin/cat  ; /bin/cat -s /etc/motd  ; success
/usr/bin/mail  ; /bin/mail -E  ; success
/usr/bin/hostname  ; hostname  ; success
/usr/bin/more  ; more audit_control  ; success
/usr/bin/cat  ; cat audit_startup  ; success
/usr/bin/man  ; man auditconfig  ; success
/sbin/sh  ; sh -c cd /usr/man; tbl /usr/man/man1m/auditconfig.1m |neqn /usr/share/lib/pub/eqnchar - |nroff -u0 -Tlp -man - | col -x > /tmp/mpTkaODb  ; success
/usr/bin/col  ; col -x  ; success
/usr/bin/tbl  ; tbl /usr/man/man1m/auditconfig.1m  ; success
/usr/bin/neqn  ; neqn /usr/share/lib/pub/eqnchar -  ; success
/usr/bin/nroff  ; nroff -u0 -Tlp -man -  ; success
/sbin/sh  ; sh -c trap '' 1 15; /usr/bin/mv -f /tmp/mpTkaODb /usr/man/cat1m/auditconfig.1m 2> /dev/null  ; success
/usr/bin/mv  ; /usr/bin/mv -f /tmp/mpTkaODb /usr/man/cat1m/auditconfig.1m  ; success
/sbin/sh  ; sh -c more -s /tmp/mpTkaODb  ; success
/usr/bin/more  ; more -s /tmp/mpTkaODb  ; success
/usr/bin/ls  ; ls  ; success
/usr/sbin/auditreduce  ; auditreduce  ; success
/usr/sbin/praudit  ; praudit  ; success
/usr/bin/su  ; su -  ; success
/sbin/sh  ; -sh  ; success
/usr/lib/fs/ufs/quota  ; /usr/sbin/quota  ; success
/usr/bin/cat  ; /bin/cat -s /etc/motd  ; success
/usr/bin/mail  ; /bin/mail -E  ; success
/usr/bin/hostname  ; hostname  ; success
/usr/sbin/auditreduce  ; auditreduce  ; success
/usr/bin/prstat  ; prstat -x  ; success
/usr/bin/sparcv9/prstat  ; prstat -x  ; success
/usr/sbin/auditreduce  ; auditreduce  ; success
/usr/sbin/praudit  ; praudit -x  ; success
/usr/sbin/auditreduce  ; auditreduce -a 20080624130000  ; success
/usr/sbin/praudit  ; praudit -x  ; success
/usr/bin/vi  ; vi /var/tmp/x  ; success
/usr/bin/grep  ; grep 2283295222 /var/tmp/x  ; success
/usr/sbin/auditreduce  ; auditreduce -a 20080624130000  ; success
/usr/sbin/praudit  ; praudit -x  ; success

Here is the sid.tcl and sid-exec.tcl

$ cat sid.tcl
#! /usr/local/bin/tclsh


if { $argc != 2 } {
        puts stderr "Usage: $argv0 <xmlfile> <user>"
        exit 1
}
set xmlfile [lindex $argv 0]
set uid [lindex $argv 1]


package require tdom


set nrec 0
set doc [dom parse [tDOM::xmlReadFile [lindex $argv 0]]]
set root [$doc documentElement]
foreach r [$root selectNodes "//subject\[@uid='$uid']"] {
        set sid [$r getAttribute sid]
        if { [info exist arraySid($sid)] } {
                incr arraySid($sid)
        } else {
                set arraySid($sid) 1
        }
}
foreach { n v } [array get arraySid] {
        puts "$n=$v"
}

$ cat sid-exec.tcl
#! /usr/local/bin/tclsh


if { $argc != 2 } {
        puts stderr "Usage: $argv0 <xmlfile> <sid>"
        exit 1
}
set xmlfile [lindex $argv 0]
set sid [lindex $argv 1]


package require tdom


set nrec 0
set doc [dom parse [tDOM::xmlReadFile [lindex $argv 0]]]
set root [$doc documentElement]
foreach r [$root selectNodes "//subject\[@sid='$sid']/.."] {
        set event [$r getAttribute event]
        if { ![string equal $event {execve(2)}] } { continue }
        foreach pNode [$r selectNodes {path/text()}] {
                puts -nonewline "[string trim [$pNode nodeValue]] "
        }
        puts -nonewline " ; "
        foreach argNode [$r selectNodes {exec_args/arg/text()}] {
                puts -nonewline "[string trim [$argNode nodeValue]] "
        }
        puts -nonewline " ; "
        puts "[[$r selectNodes {return}] getAttribute errval]"
}

Labels: , , , ,

Wednesday, October 17, 2007

Table^10

If you watch TV often enough, you will probably come across this ad about the new web site, Mocca.com (MediaCorp Online Communities and Classified Advertising).

The above site looks pretty good on my Firefox with a response time of about 3 seconds for 121 requests (413KB in total). The result is based on the Firefox Add-on, Firebug. 100+ requests is considered quite a lot.

What I normally do when I find the site "interesting" is to look at the HTML code. Guess what, I realised that there is a lot issues with the HTML source.

  • The redirection is not done properly. Normally it will be redirected to the location via the Location or Content-Location header to avoid additional overhead from browser to render the HTML code. Also, the HTML is definitely ill-formed and not complete.
    $ curl --dump-header /dev/tty http://mocca.com/
    HTTP/1.1 200 OK
    Content-Length: 61
    Content-Type: text/html
    Content-Location: http://mocca.com/index.htm
    Last-Modified: Wed, 26 Sep 2007 09:30:19 GMT
    Accept-Ranges: bytes
    ETag: "b8a39eda1f0c81:17de"
    X-Powered-By: ASP.NET
    Date: Tue, 16 Oct 2007 08:46:21 GMT
    Server: Concealed by Juniper Networks DX
    Via: 1.1 MC-LB1 (Juniper Networks Application Acceleration Platform - DX 5.2.5 0)
    Set-Cookie: rl-sticky-key=caacb80750; path=/; expires=Tue, 16 Oct 2007 09:31:03GMT
    
    <META HTTP-EQUIV="Refresh" CONTENT="1; URL=/portal/site/cas">
    
  • When I fetched the home page http://mocca.com/portal/site/cas, I realised that the HTML code has the <title> node before the <html> node. If you were to convert the HTML to a DOM tree, you may likely get 1 node in the tree. Also, I submitted the URL to W3C HTML Validation Service, and it reported 218 Errors.
    $ curl --silent http://mocca.com/portal/site/cas 2>&1 | awk '$0!~/^$/{print}' | head -10
    
    <title>MediaCorp Mocca </title>
    <html xmlns="http://www.w3.org/1999/xhtml">
            <head>
    
    <link href="/vgn-ext-templating/common/styles/vgn-ext-templating.css" rel="stylesheet" type="text/css"></link>
              <script language="JavaScript" src="/portal/jslib/form_state_manager.js"></script>
              <noscript>In order to bring you the best possible user experience, this site uses Javascript. If you are seeing this message, it is likely that the Javascript option in your browser is disabled. For optimal viewing of this site, p
    lease ensure that Javascript is enabled for your browser.
              </noscript>
                    <base target="_top">
    
  • I realised there is a lot of table within table within table. Hey, that's interesting. At the back of my mind I was wondering how deep is this nesting of tables going to be. A Tcl program with tDOM extension should do the job. Of course I have to remove the first <title> before the parsing, otherwise I will end up with 1 node in the tree.
    package require tdom
    
    proc howdeep { node } {
     set l [split [$node toXPath] /]
     set n [llength $l]
     set count 0
     for { set i 0 } { $i < $n } { incr i } {
      if { [string match -nocase "table*" [lindex $l $i]] } {
       incr count
      }
     }
     return $count
    }
    
    set html index.html-modified
    set doc [dom parse -html [tDOM::xmlReadFile $html]]
    set root [$doc documentElement]
    
    set max 0
    set maxnode {}
    foreach table [$root selectNodes {//table}] {
     set level [howdeep $table]
     if { $level > $max } {
      set max $level
      set maxnode $table
     }
    }
    puts $max
    puts [llength [$root selectNodes {//table}]]
    puts [$maxnode toXPath]
    
    Output of this program:
    10
    144
    /html/body/table/tr[2]/td[2]/table/tr[2]/td/table/tr/td[3]/table/tr/td/table/tr/td/table[1]/tr/td[1]/table/tr[2]/td[2]/table/tr/td[1]/table/tr[1]/td/table
    

Wow, we are talking about a nesting of 10 levels of <table> and a total of 144 tables, that's a lot! So, what's the conclusion.

  • Modern browsers are so forgiving and they normally do a very good job to render complex and even ill-formed HTML code
  • With such a deep nesting of tables and 100+ requests, the browser is able to render the content in seconds. That is amusing.

Labels: , , ,