Hi David,

I attach you my code, you can run the diff between the original and my code and check the differences ;) I had the same problem as you.

Regards,

El 14/09/2014 9:24, David Uihlein escribió:
I'm having trouble getting OpenVas to work. I've updated the code changing the escalators to alerts and it's creating the tasks, but the responses are coming back different than what iPF expects, so I get a warning on target creation even though it gets created

here's my log
Sep 13 18:09:51 pfcmd.pl(32545) INFO: Instantiate a new vulnerability scanning engine object of type pf::scan::openvas. (pf::scan::instantiate_scan_engine) Sep 13 18:09:51 pfcmd.pl(32545) DEBUG: Instantiating a new pf::scan::openvas scanning object (pf::scan::openvas::new) Sep 13 18:09:51 pfcmd.pl(32545) INFO: Creating a new scan target named 1410649791760d84 for host 192.168.70.11 (pf::scan::openvas::createTarget) Sep 13 18:09:51 pfcmd.pl(32545) TRACE: Scan target creation command: omp -h 127.0.0.1 -p 9390 -u admin -w pass -X '<create_target><name>1410649791760d84</name><hosts>192.168.70.11</hosts></create_target>' (pf::scan::openvas::createTarget) Sep 13 18:09:53 pfcmd.pl(32545) TRACE: Scan target creation output: <create_target_response id="6b98a0e0-51bb-406f-8936-e64adafebec6" status_text="OK, resource created" status="201"></create_target_response> (pf::scan::openvas::createTarget) Sep 13 18:09:53 pfcmd.pl(32545) WARN: There was an error creating scan target named 1410649791760d84, here's the output: <create_target_response id="6b98a0e0-51bb-406f-8936-e64adafebec6" status_text="OK, resource created" status="201"></create_target_response> (pf::scan::openvas::createTarget)


Can you tell me what I'm missing, or send me the changes you made for scan.pm and openvas.pm? (I assume those are the only 2 files that need to be modified)

No se encontraron virus en este mensaje.
Comprobado por AVG - www.avg.com <http://www.avg.com>
Versión: 2013.0.3485 / Base de datos de virus: 4015/8207 - Fecha de publicación: 09/13/14


package pf::scan::openvas;

=head1 NAME

pf::scan::openvas

=cut

=head1 DESCRIPTION

pf::scan::openvas is a module to add OpenVAS scanning option.

=cut

use strict;
use warnings;

use Log::Log4perl;
use MIME::Base64;
use Readonly;

use base ('pf::scan');

use pf::config;
use pf::util;

Readonly our $RESPONSE_OK                   => 200;
Readonly our $RESPONSE_RESOURCE_CREATED     => 201;
Readonly our $RESPONSE_REQUEST_SUBMITTED    => 202;

=head1 METHODS 

=over

=item createAlert

Create an alert which will trigger an action on the OpenVAS server once the 
scan will finish

=cut

sub createAlert {
    my ( $this ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $name = $this->{_id};
    my $callback = $this->_generateCallback();
    my $command = _get_alert_string($name, $callback);

    $logger->info("Creating a new scan alert named $name");

    my $cmd = "omp -h $this->{_host} -p $this->{_port} -u $this->{_user} -w 
$this->{_pass} -X '$command'";
    $logger->trace("Scan alert creation command: $cmd");
    my $output = pf_run($cmd);
    chomp($output);
    $logger->trace("Scan alert creation output: $output");

    # Fetch response status and alert id
    my ($alert_id, $response);
    if ($output =~ /^<create_alert_response/) {
         if ($output =~ /id="([a-zA-Z0-9\-]+)"/) {      
                $alert_id=$1;
         }
         if ($output =~ /status="([0-9]+)"/) {
                 $response=$1
         }
    }

    # Scan alert successfully created
    if ( defined($response) && $response eq $RESPONSE_RESOURCE_CREATED ) {
        $logger->info("Scan alert named $name successfully created with id: 
$alert_id");
        $this->{'_alertId'} = $alert_id;
        return $TRUE;
    }

    $logger->warn("There was an error creating scan alert named $name, here's 
the output: $output");
    return;
}


=item createTarget

Create a target (a target is a host to scan)

=cut

sub createTarget {
    my ( $this ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $name = $this->{_id};
    my $target_host = $this->{_scanIp};
    my $command = 
"<create_target><name>$name</name><hosts>$target_host</hosts></create_target>";

    $logger->info("Creating a new scan target named $name for host 
$target_host");

    my $cmd = "omp -h $this->{_host} -p $this->{_port} -u $this->{_user} -w 
$this->{_pass} -X '$command'";
    $logger->trace("Scan target creation command: $cmd");
    my $output = pf_run($cmd);
    chomp($output);
    $logger->trace("Scan target creation output: $output");

    # Fetch response status and target id
    my ($target_id, $response);
    if ($output =~ /^<create_target_response/) {
         if ($output =~ /id="([a-zA-Z0-9\-]+)"/) {      
                $target_id=$1;
         }
         if ($output =~ /status="([0-9]+)"/) {
                 $response=$1
         }
    }

    # Scan target successfully created
    if ( defined($response) && $response eq $RESPONSE_RESOURCE_CREATED ) {
        $logger->info("Scan target named $name successfully created with id: 
$target_id");
        $this->{'_targetId'} = $target_id;
        return $TRUE;
    }

    $logger->warn("There was an error creating scan target named $name, here's 
the output: $output");
    return;
}

=item createTask

Create a task (a task is a scan) with the existing config id and previously 
created target and alert

=cut

sub createTask {
    my ( $this )  = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $name = $this->{_id};

    $logger->info("Creating a new scan task named $name");

    my $command = _get_task_string(
        $name, $Config{'scan'}{'openvas_configid'}, $this->{_targetId}, 
$this->{_alertId}
    );
    my $cmd = "omp -h $this->{_host} -p $this->{_port} -u $this->{_user} -w 
$this->{_pass} -X '$command'";
    $logger->trace("Scan task creation command: $cmd");
    my $output = pf_run($cmd);
    chomp($output);

    # Fetch response status and task id
    my ($task_id, $response);
    if ($output =~ /^<create_task_response/) {
         if ($output =~ /id="([a-zA-Z0-9\-]+)"/) {      
                $task_id=$1;
         }
         if ($output =~ /status="([0-9]+)"/) {
                 $response=$1
         }
    }

    # Scan task successfully created
    if ( defined($response) && $response eq $RESPONSE_RESOURCE_CREATED ) {
        $logger->info("Scan task named $name successfully created with id: 
$task_id");
        $this->{'_taskId'} = $task_id;
        return $TRUE;
    }

    $logger->warn("There was an error creating scan task named $name, here's 
the output: $output");
    return;
}

=item processReport

Retrieve the report associated with a task. 
When retrieving a report in other format than XML, we received the report in 
base64 encoding.

Report processing's duty is to ensure that the proper violation will be 
triggered.

=cut

sub processReport {
    my ( $this ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $name                = $this->{_id};
    my $report_id           = $this->{_reportId};
    my $report_format_id    = $Config{'scan'}{'openvas_reportformatid'}; 
    my $command             = "<get_reports report_id=\"$report_id\" 
format_id=\"$report_format_id\"/>";

    $logger->info("Getting the scan report for the finished scan task named 
$name");

    my $cmd = "omp -h $this->{_host} -p $this->{_port} -u $this->{_user} -w 
$this->{_pass} -X '$command'";
    $logger->trace("Report fetching command: $cmd");
    my $output = pf_run($cmd);
    chomp($output);
    $logger->trace("Report fetching output: $output");
    $logger->warn("Report fetching output: $output");

    # Fetch response status and report
    my ($raw_report, $response);
    if ($output =~ /^<get_reports_response/) {
         if ($output =~ /content_type="text\/plain">([a-zA-Z0-9\=]+)</) {       
                $raw_report=$1;
         }
         if ($output =~ /status="([0-9]+)"/) {
                 $response=$1
         }
    }

    # Scan report successfully fetched
    if ( defined($response) && $response eq $RESPONSE_OK && 
defined($raw_report) ) {
        $logger->info("Report id $report_id successfully fetched for task named 
$name");
        $this->{_report} = decode_base64($raw_report);   # we need to decode 
the base64 report

        # We need to manipulate the scan report.
        # Each line of the scan report is pushed into an arrayref
        $this->{'_report'} = [ split("\n", $this->{'_report'}) ];
        pf::scan::parse_scan_report($this);

        return $TRUE;
    }

    $logger->warn("There was an error fetching the scan report for the task 
named $name, here's the output: $output");
    return;
}

=item new

Create a new Openvas scanning object with the required attributes

=cut

sub new {
    my ( $class, %data ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    $logger->debug("Instantiating a new pf::scan::openvas scanning object");

    my $this = bless {
            '_id'               => undef,
            '_host'             => $Config{'scan'}{'host'},
            '_port'             => undef,
            '_user'             => $Config{'scan'}{'user'},
            '_pass'             => $Config{'scan'}{'pass'},
            '_scanIp'           => undef,
            '_scanMac'          => undef,
            '_report'           => undef,
            '_configId'         => undef,
            '_reportFormatId'   => undef,
            '_targetId'         => undef,
            '_alertId'      => undef,
            '_taskId'           => undef,
            '_reportId'         => undef,
            '_status'           => undef,
            '_type'             => undef,
    }, $class;

    foreach my $value ( keys %data ) {
        $this->{'_' . $value} = $data{$value};
    }

    # OpenVAS specific attributes
    $this->{_port} = $Config{'scan'}{'openvas_port'};
    $this->{_configId} = $Config{'scan'}{'openvas_configid'};
    $this->{_reportFormatId} = $Config{'scan'}{'openvas_reportformatid'};

    return $this;
}

=item startScan

That's where we use all of these method to run a scan

=cut

sub startScan {
    my ( $this ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    $this->createTarget();
    $this->createAlert();
    $this->createTask();
    $this->startTask();
}

=item startTask

Start a scanning task with the previously created target and alert

=cut

sub startTask {
    my ( $this ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $name    = $this->{_id};
    my $task_id = $this->{_taskId};
    my $command = "<start_task task_id=\"$task_id\"/>";

    $logger->info("Starting scan task named $name");

    my $cmd = "omp -h $this->{_host} -p $this->{_port} -u $this->{_user} -w 
$this->{_pass} -X '$command'";
    $logger->trace("Scan task starting command: $cmd");
    my $output = pf_run($cmd);
    chomp($output);
    $logger->trace("Scan task starting output: $output");

    # Fetch response status and report id
    my ($report_id, $response);
    if ($output =~ /^<start_task_response/) {
         if ($output =~ /<report_id>([a-zA-Z0-9\-]+)</) {       
                $report_id=$1;
         }
         if ($output =~ /status="([0-9]+)"/) {
                 $response=$1
         }
    }

    # Scan task successfully started
    if ( defined($response) && $response eq $RESPONSE_REQUEST_SUBMITTED ) {
        $logger->info("Scan task named $name successfully started");
        $this->{'_reportId'} = $report_id;
        $this->{'_status'} = $pf::scan::STATUS_STARTED;
        $this->statusReportSyncToDb();
        return $TRUE;
    }

    $logger->warn("There was an error starting the scan task named $name, 
here's the output: $output");
    return;
}

=item _generateCallback

Escalator callback needs to be different if we are running OpenVAS locally or 
remotely.

Local: plain HTTP on loopback (127.0.0.1)

Remote: HTTPS with fully qualified domain name on admin interface

=cut

sub _generateCallback {
    my ( $this ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $name = $this->{'_id'};
    my $callback = "<method>HTTP Get<data>";
    if ($this->{'_host'} eq '127.0.0.1') {
        $callback .= "http://127.0.0.1/scan/scan_fetch_report.cgi?scanid=$name";;
    }
    else {
        $callback .= 
"https://$Config{general}{hostname}.$Config{general}{domain}:$Config{ports}{admin}/scan/scan_fetch_report.cgi?scanid=$name";;
    }
    $callback .= "<name>URL</name></data></method>";

    $logger->debug("Generated OpenVAS callback is: $callback");
    return $callback;
}

=back

=head1 SUBROUTINES

=over

=item _get_alert_string

create_alert string creation.

=cut

sub _get_alert_string {
    my ($name, $callback) = @_;

    return <<"EOF";
<create_alert>
  <name>$name</name>
  <condition>Always</condition>
  <event>Task run status changed<data>Done<name>status</name></data></event>
  $callback
</create_alert>
EOF
}

=item _get_task_string

create_task string creation.

=cut

sub _get_task_string {
    my ($name, $config_id, $target_id, $alert_id) = @_;

    return <<"EOF";
<create_task>
  <name>$name</name>
  <config id=\"$config_id\"/>
  <target id=\"$target_id\"/>
  <alert id=\"$alert_id\"/>
</create_task>
EOF
}

=back

=head1 AUTHOR

Inverse inc. <i...@inverse.ca>

=head1 COPYRIGHT

Copyright (C) 2005-2013 Inverse inc.

=head1 LICENSE

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
USA.

=cut

1;
package pf::scan;

=head1 NAME

pf::scan - Module that performs the vulnerability scan operations

=cut

=head1 DESCRIPTION

pf::scan contains the general functions required to lauch and complete a 
vulnerability scan on a host

=cut

use strict;
use warnings;

use Log::Log4perl;
use Parse::Nessus::NBE;
use Readonly;
use Try::Tiny;

use overload '""' => "toString";

BEGIN {
    use Exporter ();
    our (@ISA, @EXPORT, @EXPORT_OK);
    @ISA = qw(Exporter);
    @EXPORT = qw(run_scan $SCAN_VID $scan_db_prepared scan_db_prepare);
    @EXPORT_OK = qw(scan_insert_sql scan_select_sql scan_update_status_sql);
}

use pf::config;
use pf::db;
use pf::iplog qw(ip2mac);
use pf::scan::nessus;
use pf::scan::openvas;
use pf::util;
use pf::violation qw(violation_exist_open violation_trigger violation_modify);

Readonly our $SCAN_VID          => 1200001;
Readonly our $SEVERITY_HOLE     => 1;
Readonly our $SEVERITY_WARNING  => 2;
Readonly our $SEVERITY_INFO     => 3;
Readonly our $STATUS_NEW => 'new';
Readonly our $STATUS_STARTED => 'started';
Readonly our $STATUS_CLOSED => 'closed';

# DATABASE HANDLING
use constant SCAN       => 'scan';
our $scan_db_prepared   = 0;
our $scan_statements    = {};

sub scan_db_prepare {
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    $logger->debug("Preparing database statements.");

    $scan_statements->{'scan_insert_sql'} = get_db_handle()->prepare(qq[
            INSERT INTO scan (
                id, ip, mac, type, start_date, update_date, status, report_id
            ) VALUES (
                ?, ?, ?, ?, ?, ?, ?, ?
            )
    ]);

    $scan_statements->{'scan_select_sql'} = get_db_handle()->prepare(qq[
            SELECT id, ip, mac, type, start_date, update_date, status, report_id
            FROM scan
            WHERE id = ?
    ]);

    $scan_statements->{'scan_update_sql'} = get_db_handle()->prepare(qq[
            UPDATE scan SET
                status = ?, report_id =?
            WHERE id = ?
    ]);

    $scan_db_prepared = 1;
    return 1;
}


=head1 SUBROUTINES

=over

=item instantiate_scan_engine

Instantiate the correct vulnerability scanning engine with attributes

=cut

sub instantiate_scan_engine {
    my ( $type, %scan_attributes ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $scan_engine = 'pf::scan::' . $type;
    $logger->info("Instantiate a new vulnerability scanning engine object of 
type $scan_engine.");
    $scan_engine = untaint_chain($scan_engine);
    try {
        # try to import module and re-throw the error to catch if there's one
        eval "$scan_engine->require()";
        die($@) if ($@);

    } catch {
        chomp($_);
        $logger->error("Initialization of scan engine $scan_engine failed: $_");
    };

    return $scan_engine->new(%scan_attributes);
}

=item parse_scan_report

Parse a scan report from the scan object and trigger violations if needed

=cut

sub parse_scan_report {
    my ( $scan ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    $logger->debug("Scan report to analyze. Scan id: $scan"); 

    my $scan_report = $scan->getReport();
    my @count_vulns = (
        Parse::Nessus::NBE::nstatvulns(@$scan_report, $SEVERITY_HOLE),
        Parse::Nessus::NBE::nstatvulns(@$scan_report, $SEVERITY_WARNING),
        Parse::Nessus::NBE::nstatvulns(@$scan_report, $SEVERITY_INFO),
    );

    # FIXME we shouldn't poke directly into the scan object, we should rely on 
accessors
    # we are slicing out the parameters out of the $scan objectified hashref
    my ($mac, $ip, $type) = @{$scan}{qw(_scanMac _scanIp _type)};

    # Trigger a violation for each vulnerability
    my $failed_scan = 0;    
    foreach my $current_vuln (@count_vulns) {
        # Parse nstatvulns format
        my ( $trigger_id, $number ) = split(/\|/, $current_vuln);

        $logger->info("Calling violation_trigger for ip: $ip, mac: $mac, type: 
$type, trigger: $trigger_id");
        my $violation_added = violation_trigger($mac, $trigger_id, $type);

        # If a violation has been added, consider the scan failed
        if ( $violation_added ) {
            $failed_scan = 1;
        }
    }

    # If scan is requested because of registration scanning
    #   Clear scan violation if the host didn't generate any violation
    #   Otherwise we keep the violation and clear the ticket_ref (so we can 
re-scan once he remediates)
    # If the scan came from elsewhere
    #   Do nothing

    # The way we accomplish the above workflow is to differentiate by checking 
if special violation exists or not
    if ( my $violation_id = violation_exist_open($mac, $SCAN_VID) ) {
        $logger->trace("Scan is completed and there is an open scan violation. 
We have something to do!");

        # We passed the scan so we can close the scan violation
        if ( !$failed_scan ) {
            my $cmd = $bin_dir . "/pfcmd manage vclose $mac $SCAN_VID";
            $logger->info("Calling $cmd");
            my $grace = pf_run("$cmd");
            # FIXME shouldn't we focus on return code instead of output? pretty 
sure this is broken
            if ( $grace == -1 ) {
                $logger->warn("Problem trying to close scan violation");
                return;
            }

        # Scan completed but a violation has been found
        # HACK: we empty the violation's ticket_ref field which we use to track 
if scan is in progress or not
        } else {
            $logger->debug("Modifying violation id $violation_id to empty its 
ticket_ref field");
            violation_modify($violation_id, (ticket_ref => ""));
        }
    }

    $scan->setStatus($STATUS_CLOSED);
    $scan->statusReportSyncToDb();
}

=item retrieve_scan

Retrieve a scan object populated from the database using the scan id

=cut

sub retrieve_scan {
    my ( $scan_id ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    my $query = db_query_execute(SCAN, $scan_statements, 'scan_select_sql', 
$scan_id) || return 0;
    my $scan_infos = $query->fetchrow_hashref();
    $query->finish();

    if (!defined($scan_infos) || $scan_infos->{'id'} ne $scan_id) {
        $logger->warn("Invalid scan object requested");
        return;
    }

    my %scan_args;
    # here we map parameters expected by the object (left) with fields of the 
database (right)
    @scan_args{qw(id scanIp scanMac reportId status type)} = @$scan_infos{qw(id 
ip mac report_id status type)};
    my $scan = instantiate_scan_engine($scan_infos->{'type'}, %scan_args);

    return $scan;
}

=item run_scan

Prepare the scan attributes, call the engine instantiation and start the scan

=cut

sub run_scan {
    my ( $host_ip ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    $host_ip =~ s/\//\\/g;          # escape slashes
    $host_ip = clean_ip($host_ip);  # untainting ip

    # Resolve mac address
    my $host_mac = ip2mac($host_ip);
    if ( !$host_mac ) {
        $logger->warn("Unable to find MAC address for the scanned host 
$host_ip. Scan aborted.");
        return;
    }

    # Preparing the scan attributes
    my $epoch   = time;
    my $date    = POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime($epoch));
    my $id      = generate_id($epoch, $host_mac);
    my $type    = lc($Config{'scan'}{'engine'});

    # Check the scan engine
    # If set to "none" we abort the scan
    if ( $type eq "none" ) {
        return;
    }

    my %scan_attributes = (
            id         => $id,
            scanIp     => $host_ip,
            scanMac    => $host_mac,
            type       => $type,
    );

    db_query_execute(SCAN, $scan_statements, 'scan_insert_sql',
            $id, $host_ip, $host_mac, $type, $date, '0000-00-00 00:00:00', 
$STATUS_NEW, 'NULL'
    ) || return 0;

    # Instantiate the new scan object
    my $scan = instantiate_scan_engine($type, %scan_attributes);

    # Start the scan
    # IBB Patch 2014-08-09 - Juan 
    my $error_scan = $scan->startScan();
 
    # Hum ... somethings wrong in the scan ?
    if ( !$error_scan ) {
        my $cmd = $bin_dir . "/pfcmd manage vclose $host_mac $SCAN_VID";
        $logger->info("Calling $cmd");
        my $grace = pf_run("$cmd");
        # FIXME shouldn't we focus on return code instead of output? pretty 
sure this is broken
        if ( $grace == -1 ) {
            $logger->warn("Problem trying to close scan violation");
        }
    }
}

=back

=head1 METHODS

We are also a lean base class for pf::scan::*.

=over

=item statusReportSyncToDb

Update the status and reportId of the scan in the database.

=cut

sub statusReportSyncToDb {
    my ( $self ) = @_;
    my $logger = Log::Log4perl::get_logger(__PACKAGE__);

    db_query_execute(SCAN, $scan_statements, 'scan_update_sql', 
        $self->{'_status'}, $self->{'_reportId'}, $self->{'_id'}
    ) || return 0;
    return $TRUE;
}

=item isNotExpired

Returns true or false based on wether scan is considered expired or not.

This basically means can we still apply the result of a scan to a node or was 
it already applied.

=cut

sub isNotExpired {
    my ($self) = @_;
    return ($self->{'_status'} eq $STATUS_STARTED);
}

sub setStatus {
    my ($self, $status) = @_;
    $self->{'_status'} = $status;
    return $TRUE;
}

sub getReport {
    my ($self) = @_;
    return $self->{'_report'};
}

sub toString {
    my ($self) = @_;
    return $self->{'_id'};
}

=back

=head1 AUTHOR

Inverse inc. <i...@inverse.ca>

=head1 COPYRIGHT

Copyright (C) 2005-2013 Inverse inc.

=head1 LICENSE

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
USA.

=cut


1;
------------------------------------------------------------------------------
Want excitement?
Manually upgrade your production database.
When you want reliability, choose Perforce
Perforce version control. Predictably reliable.
http://pubads.g.doubleclick.net/gampad/clk?id=157508191&iu=/4140/ostg.clktrk
_______________________________________________
PacketFence-devel mailing list
PacketFence-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/packetfence-devel

Reply via email to