segunda-feira, 25 de agosto de 2014

XML MiniDom parsing using Python 3

Ae cambada! Essa postagem é pra mostrar como funciona o XML parse usando o python3 (não tem muita diferença para as versões inferiores). Se nunca leu nada sobre xml parsing, fica tranquilo que agora vai manjar pelo menos como funciona e os conceitos básicos para a manipulação.

Para essa função o python agrega um módulo chamado MiniDOM (Minimal Document Object Model). Ele tem uma função bem simples de fazer o parse, manja o módulo HTMLParser? Já? Então, é quase a mesma coisa, do mesmo que html tem suas estruturas de marcações:

<!DOCTYPE html>
<html>
<head>
  <title>Slayer Owner</title>
</head>
<body bcolor='black'>
  <h1 style='text-align:center'>Security Analysis</h1>
  <pre> I'm listen Megadeth right now! :)</pre>
</body>
</html>

O XML segue quase o mesmo, a diferença é que xml não segue um padrão de tag, vai do gosto de quem está criando e óbvio que se quiser manipular-los terá que saber de suas estruturas.
A estrutura hierárquica, comumente são elementos(tags) dentro de outros elementos, por exemplo:

<?xml version="1.0"?>
<google> <!-- Elemento Google -->
  <blogger> <!-- Elemento blogger dentro do elemento Google-->
    <title>Blogspot</title> <!-- Tag title dentro do blogger-->
    <author>Slayer Owner</author> <!-- Tag author dentro do blogger-->
    <url>slayerowner.blogspot.com.br</url> <!-- Tag url dentro do blogger-->
    <gender>Security & Programming</gender> <!-- Tag gender dentro do blogger-->
  </blogger> <!-- Termina o elementro Blogger-->
</google> <!-- Termina o primeiro elemento: Google-->
<!-- Logo, termina o script XML-->

Na primeira linha temos a declaração do XML e a versão sendo utilizada, pós a declaração seguem os elementos, é como um HTML, abriu <TAG>, fechou </TAG>. Um caso de curiosidade, CDATA é para escapar dos caracteres especiais convertidos para html entity. Exemplo:

<![CDATA[slayer > metallica ]]>
O output vai disso vai ser...
slayer > metallica

Fora o CDATA, o output ficaria..
slayer &gt; metallica

Qualquer coisa só dar uma lida sobre html entities.
Aproveite pra analisar o rss.xml (http://www.exploit-db.com/rss.xml) do exploit-db, logo criar uma ferramenta bem babaquinha pra poder pegar o esquema de xml parsing.
Um exemplo de como funciona o parse xml:
    import libs
    rss_xml = 'rss.xml'
    parse_xml = parseString(rss_xml.read())
    parse_xml[ 
      parse_xml.channel[
        channel.item[
           <title>
           [webapps] - D-Link AP 3200 Multiple Vulnerabilities
           </title>
           <link>http://www.exploit-db.com/exploits/34206</link>
           <description>D-Link AP 3200 Multiple Vulnerabilities</description>
           <category>webapps</category>
           <pubDate>Wed, 30 Jul 2014 00:00:00 +0000</pubDate>
           <guid isPermaLink="false">edb-34206</guid>
         ]
        ]
       ]
2* rss_xml é uma variável que recebeu a source do rss.xml
3* a variável passa à ser possível ser manipulada, podendo ser filtrado qualquer coisa.
4* parse_xml se passa por rss do rss.xml, em outras palavras, ela ganha todos os elementos, tags e etc.. tudo que contém dentro da tag <rss>, observa que dentro da tag dela, está alocada todas as outras..
<rss xmlns
bla bla bla
....
bla bla bla
</rss>

5* "parse_xml.channel" quer dizer que channel está dentro de parse_xml (ressaltando que parse_xml é a tag rss). O que você consegue dentro da channel?

+------------------RSS-------------------------+
| +---------------- Channel------------------+ |
| |        +-------------Item-------------+  | |
| |        |                              |  | |
| |        |            <title>           |  | |
| |        |            <link>            |  | |
| |        |            <desc>            |  | |
| |        |                              |  | |
| |        +-------------Item-------------+  | |
| +---------------- Channel------------------+ |
+------------------RSS-------------------------+
6* Visualizou o esboço, já manja como acontece? Para cada elemento ser filtrado, tem que tornar-lo visível. Ou seja, pra dar o print() no <title> tem que declarar o Rss, Channel e Item, que através do item você consegue puxar as informações.

Primeiro pegaremos a source com o python, que não é nenhum bixo de sete cabeças e logo de cara já sabe que iremos precisar de fazer um request http e de uma lib que manipule o xml pra facilitar a vida, que é o xml.dom (e se não tiver? acho que usar o regex seria uma boa opção, porém para o script ser "bonitinho", com algumas criações de "libs caseiras", seria bem legal também).

import urllib.request
rss_xml = urllib.request.urlopen('http://www.exploit-db.com/rss.xml')
source_xml = parseString(source_xml.read())

Palavras do próprio python.org, diz que este módulo não é seguro, mas como é para uma ferramenta de verificação de updates atráves da source, apenas filtrando strings esta tranquilo... não teremos problemas com WAF.

from xml.dom.minidom import parseString

Pra você ter uma pequena ideia do que está sendo feito, crie um arquivo txt ou xml, ou melhor, salve como xml o "rss.xml" na desktop, importe os modules e olhe o exemplo adicionando:

edb_rss = source_xml.getElementsByTagName('rss')[0]#.toxml()
#print(edb_rss)
"""
Se descomentar e deixar como comentário toda a linha de baixo, 
perceba que irá retornar o valor completo do arquivo XML.
"""
channel_tag = edb_rss.getElementsByTagName('channel')[0]
item_tag = channel_tag.getElementsByTagName('item')[0]
title_tag = item_tag.getElementsByTagName('title')

for exploits in title_tag:
    print(exploits)


veja o resultado ao rodar o script
clique na imagem para ampliar

Note que foi retornado 11 linhas informando o "DOM Element: title at 0x????". Mas que merda de mensagem é essa? Bem, o DOM é o que já foi dito Document Object Model, o elemento se refere ao DOM, o title seria o nome da tag sendo buscada no xml e o que está em hex, é o valor que faz referência à tag. E aí? Notou como funciona a parada? Vamos fazer o print dos updates então. :)

Ah! Se você entendeu o bug do script, parabéns por pegar o esquema rápido :)
Caso não entendeu o fato do script estar errado deste modo, compreenda o seguinte.. Na linha 16 (na imagem), como lá mesmo já foi explicado o porquê do erro, a outra coisa que irá te ferrar mais pra frente é caso queira puxar apenas 1 linha do update.. tipo de 11 linhas, você quer pegar a linha 5, como vai fazer isso se você não tem o controle do script? Você programou, beleza.. mas não está sabendo manipular-los, além do mais se preferir puxar do source_xml, as demais acima são inúteis.
Para arrumar só seguir o padrão, se o certo é puxar pelo item, então só mudar o source_xml para item_tag.

clique na imagem para ampliar

Já temos o controle dentro do title, o que resta então é filtrar os conteúdos setando pela tag, usando getElementsByTagName, da um ligue:

title = recent_updates.getElementsByTagName('title')[0].firstChild.data
link = recent_updates.getElementsByTagName('link')[0].firstChild.data
pubDate = recent_updates.getElementsByTagName('pubDate')[0].firstChild.data

Agora só fazer um for pra imprimir tudo..

for recent_updates in item_tag:
    title = recent_updates.getElementsByTagName('title')[0].firstChild.data
    link = recent_updates.getElementsByTagName('link')[0].firstChild.data
    pubDate = recent_updates.getElementsByTagName('pubDate')[0].firstChild.data
    print(title+"\n"+link+"\n"+pubDate+"\n")

resultado da execução
clique na imagem para ampliar

Mas eu não curti esse "+0000", ignorei ele utilizando o regex
clique na imagem para ampliar

Já era mano, agora só rodar quando quiser saber se houve update de exploits, lógico isso é para tomar como base um XML parse, ainda da pra adicionar muitas coisas, por exemplo fazer abrir o Link com algum tipo de input, se levar à sério e deixar mais detalhado, adicione um optparse, por aí vai.. só ficar implementando recursos novos e que uma hora o script fica completo.

Quando eu tiver tempo eu libero o script mais completo no GitHub ;)
Valeu por lerem e até a próxima.