FriendsOfPHP/security-advisories

SilverStripe security advisories should come from the official RSS feed

phptek opened this issue · 4 comments

At the moment, this package bakes-in YML files comprising official security advisories for SilverStripe (and a bunch of other applications). This seems a pretty high-maintenance procedure for a kind of data that is by its very nature, constantly evolving.

Is there a reason why the package doesn't make use of web-APIs and RSS feeds where available for each of the supported packages? The SilverStripe one is here: https://www.silverstripe.org/download/security-releases/rss

web-APIs and RSS feeds where available

Those feeds are designed for human consumption, not for machine consumption.

In a few lines of Python, I can parse that feed and compare with a project's composer.lock.

#!/usr/bin/python
#
# Russell Michell 2018 <russellmichell@catalyst.net.nz>
#
# What is this?
#
# Parses the SilverStripe Security Releases RSS feed for vulnerabilities in the current project's
# composer.lock file.
# 
# Requirements:
#
# Feedparser:    pip install feedparser
# BeautifulSoup: apt-get install python-bs4

from bs4 import BeautifulSoup
import json, sys, feedparser, re

feed_url = 'https://www.silverstripe.org/download/security-releases/rss'

# We use this pseudo constant when no package is given in the RSS advisories
DEFAULT_PACKAGE = 'silverstripe/framework'

# Clean up version constraints
def clean_version(input):
    return re.sub('[^[\d.]', '', input)

# Cleanup package-names
def clean_package(input):
    return input.strip(' :')

with open('./composer.lock', 'r') as composerLockFile:
    json = json.load(composerLockFile)

    feed = feedparser.parse(feed_url)

    if feed.channel == None:
        print 'No feed data found.'
        sys.exit(1)

    for package in json['packages']:
        c_package = package['name']
        c_version = clean_version(package['version'])

        for item in feed['items']:
            soup = BeautifulSoup(item['description'], 'html.parser')
            # All entity-encoded <dd> HTML elements found within an RSS <description> element
            allDDs = soup.find_all('dd')
            # TODO Can also use BS4 to access via class="foo" rather than as a dict/numeric-index
            severity = allDDs[0].string
            identifier = allDDs[1].string
            versionsAffected = allDDs[2].string
            versionsFixed = allDDs[3].string
            advisory_link = item.link

            # Advisories sometimes exclude the package name. Assume this means "silverstripe/framework"
            # Advisories are formatted slightly differently (with/without colon-separated spaces between <package><version>)
            package = re.sub("[\s:]?((>=?)?\s?)?(\d\.?)+(-rc\d)?,?", '', versionsFixed)
            version = re.sub("([^\/]\w+\/[\w-]+(?=[\s:]))+", '', versionsFixed)
            f_package = clean_package(package)
            f_version = clean_version(version)

            # Basic test to see if a package has been stipulated. If not, we assume: "silverstripe/framework"
            if f_package is None or '/' not in f_package:
                f_package = DEFAULT_PACKAGE

            if c_package != f_package:
                continue

            for fixed_version in f_version.split(','):
                is_vulnerable = c_version < fixed_version

                if is_vulnerable:
                    print '[ALERT] %s version %s has a security advisory: %s' % (c_package, c_version, advisory_link)
                    # Break so no further versions need to be checked
                    break
                    sys.exit(1)

I'm going to close this one as we don't have any resources to maintain such a script and it is not our responsibility to create entries for all PHP projects out there. It's a contribution based effort from the community. Now, if the Silverstripe community wants to automate the process of submitting their advisories info, they can do so of course.