Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: [website] Add new fields to User model #73

Merged
merged 21 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3a1cc60
new: [website] Add new fields to User model: country_code, bio, webpa…
cedricbonhomme Sep 11, 2024
418744b
chg: [website] Added a link to the profile edition page from the prof…
cedricbonhomme Sep 11, 2024
a2121e5
chg: [website] Improved user's profile page.
cedricbonhomme Sep 11, 2024
fbf26a1
fix: [website] Mare sure the list of comments and bundles have unique…
cedricbonhomme Sep 11, 2024
c2922df
chg: [website] Nake the profile picture bigger.
cedricbonhomme Sep 11, 2024
467c252
chg: [website] Used the function src_requst_ip in order to get the re…
cedricbonhomme Sep 12, 2024
6b4170d
chg: Updated CHANGELOG file.
cedricbonhomme Sep 12, 2024
052d178
chg: [website] Remove the div dedicated to the profile avatar if not …
cedricbonhomme Sep 12, 2024
f1a5a4f
chg: [website] Improved model validators and forms.
cedricbonhomme Sep 12, 2024
81fb731
chg: [website] Improved user profile page and added function to get t…
cedricbonhomme Sep 12, 2024
bf5dcfb
chg: [website] Do not display the button to create a new advisory for…
cedricbonhomme Sep 13, 2024
aeab393
chg: [website] Added the empty choice in the list of country code.
cedricbonhomme Sep 13, 2024
72de2c8
chg: [website] Updated user profile page.
cedricbonhomme Sep 13, 2024
459cf7e
chg: [release] Cosmethic change to the CHANGELOG.
cedricbonhomme Sep 13, 2024
75ee913
chg: [website] Added a function to backup the database with pg_dump. …
cedricbonhomme Sep 13, 2024
c02affb
chg: [release] Updated CHANGELOG.
cedricbonhomme Sep 13, 2024
b8ca9db
chg: [bin] Only backup and update is generic.user_accounts is set to …
cedricbonhomme Sep 13, 2024
432e681
chg: [website] Harmonmisation of the term used for the social account…
cedricbonhomme Sep 16, 2024
74112f3
chg: [dependencies] Updated Python dependencies.
cedricbonhomme Sep 16, 2024
1c4601e
chg: [website] Improved the view dedicated to the directory of users.
cedricbonhomme Sep 16, 2024
9603abe
chg: [website] Removed useless imports.
cedricbonhomme Sep 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
Vulnerability Lookup Changelog
==============================

## 1.6.0 (not yet released)

### News

- Importer for Tailscale vulnerabilities
[#68](https://github.com/cve-search/vulnerability-lookup/issues/68)
- New user profile page with more information and detection of the country
during user sign-up using the CIRCL MMDB service
[#73](https://github.com/cve-search/vulnerability-lookup/pull/73)
- Added the ability to filter comments by any taxonomy tags by clicking on the corresponding badge
([b3e0bdf](https://github.com/cve-search/vulnerability-lookup/commit/b3e0bdf6e15b5821c3e7a4e574fa452857fd0410))
- Implemented a function to back up the database using pg_dump. This function is automatically triggered by the
project's update command, ensuring a backup is created before any database upgrades take place.
([75ee913](https://github.com/cve-search/vulnerability-lookup/commit/75ee9132beb9df1c5a847eb11488dd5241f3ee1f))


### Improvements

- [API] Enhanced detection of CVE, GHSA, and PySec IDs within bundle descriptions and comments.
This enables automatic identification of related vulnerabilities linked to a comment or a bundle.
([2c00695](https://github.com/cve-search/vulnerability-lookup/commit/2c00695864dafc20de6394dc0fe7e3d02ff570f1),
[162a599](https://github.com/cve-search/vulnerability-lookup/commit/162a599ac12482e3cfb6e0d6d9d1566ef10dab01),
[401d780](https://github.com/cve-search/vulnerability-lookup/commit/401d78081f4aa6c14664a74aaf4e3c4398d05a33)).
- Added more validation to the various attributes of the User model.
([758e571](https://github.com/cve-search/vulnerability-lookup/commit/758e5710282434e30ba61ed9efeea070d94407c1),
[3a1cc60](https://github.com/cve-search/vulnerability-lookup/commit/3a1cc604ca1a2ba642ead2b68f9e36ee6a641326))
- Simplified search page
([f2c55bc](https://github.com/cve-search/vulnerability-lookup/commit/f2c55bc9f7e53caca4d011e06678989c490e9ba2))
- Improved display of tables and lists generated from Markdown (in comments and bundles)
([24fa4f9](https://github.com/cve-search/vulnerability-lookup/commit/24fa4f95ed4e0266424e71d5d985ae693f22e9e1),
[15fe9b2](https://github.com/cve-search/vulnerability-lookup/commit/15fe9b2c673ecf1ab14065e4925869d061dccebe))
- The ranking of the users is now taking into account the contributions of comments and bundles.
Users who have never contributed are sorted by *last_seen*, after the processed result.
([4e4a436](https://github.com/cve-search/vulnerability-lookup/commit/4e4a436ea6052bb23b7bad373c010bf161fff05e))
- Various graphical and accessibility improvements.

### Fixes

- Do not iterate over meta tags when never defined in an object
([93f9966](https://github.com/cve-search/vulnerability-lookup/commit/93f9966ad4009d8bbe8b54b7efb87a19886e3605))


## 1.5.0 (2024-08-30)

### News
Expand Down
8 changes: 5 additions & 3 deletions bin/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ def main() -> None:
keep_going(args.yes)
run_command(f'poetry run {(Path("tools") / "validate_config_files.py").as_posix()} --update')

print('* Migrate database.')
keep_going(args.yes)
run_command('poetry run flask --app website.app db upgrade')
if get_config('generic', 'user_accounts'):
print('* Migrate database.')
keep_going(args.yes)
run_command('poetry run flask --app website.app db_backup')
run_command('poetry run flask --app website.app db upgrade')

print('* Restarting')
keep_going(args.yes)
Expand Down
566 changes: 294 additions & 272 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "vulnerabilitylookup"
version = "1.5.0"
version = "1.6.0"
description = "Vulnerability Lookup facilitates quick correlation of vulnerabilities from various sources, independent of vulnerability IDs, and streamlines the management of Coordinated Vulnerability Disclosure (CVD)."
authors = ["Alexandre Dulaunoy <alexandre.dulaunoy@circl.lu>", "Raphaël Vinot <raphael.vinot@circl.lu>", "Cédric Bonhomme <cedric.bonhomme@circl.lu>"]
license = "AGPL-3.0-or-later"
Expand Down Expand Up @@ -69,6 +69,7 @@ the-big-username-blacklist = "^1.5.4"
cvss = "^3.1"
xmltodict = "^0.13.0"
feedparser = "^6.0.11"
pycountry = "^24.6.1"

[tool.poetry.group.dev.dependencies]
ipython = "^8.26.0"
Expand Down
1 change: 1 addition & 0 deletions website/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
# ##### Registering commands

application.cli.add_command(commands.db_init)
application.cli.add_command(commands.db_backup)
application.cli.add_command(commands.create_admin)
application.cli.add_command(commands.create_user)
application.cli.add_command(commands.user_list)
Expand Down
47 changes: 47 additions & 0 deletions website/lib/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from typing import Any
from typing import List

import re
import requests

from sqlalchemy import func, desc

from website.models import Bundle
from website.models import Comment
from website.models import User
from website.web.bootstrap import db


def find_cve_ids(text: str) -> List[str]:
Expand Down Expand Up @@ -43,3 +52,41 @@ def find_pysec_ids(text: str) -> List[str]:
pysec_ids = list(set(pysec_ids))

return pysec_ids


def query_country_code_mmdb(ip_address: str) -> str:
"""Query the MMDB server from CIRCL in order to get the Aplha-3 code from an IP address."""
url = f"https://ip.circl.lu/geolookup/{ip_address}"
try:
response = requests.get(url)
except Exception:
return ""
if response.status_code == 200:
try:
return response.json()[0]["country_info"]["Alpha-3 code"]
except Exception:
return ""
else:
return ""


def top_contributors(limit: int = 3) -> List[Any]:
"""Return the list of the top 3 (by default) contributors (creation of comments and bundles)."""
top_contributors = (
db.session.query(
User.id,
User.login,
(func.count(Comment.uuid) + func.count(Bundle.uuid)).label(
"total_contributions"
),
)
.outerjoin(Comment, User.id == Comment.author_id) # Join User with Comment
.outerjoin(Bundle, User.id == Bundle.author_id) # Join User with Bundle
.group_by(User.id) # Group by User ID to aggregate counts
.order_by(desc("total_contributions")) # Order by total contributions
)
if limit != -1:
top_contributors = top_contributors.limit(
limit
) # Limit to the top 3 (by default) contributors
return top_contributors.all()
81 changes: 81 additions & 0 deletions website/migrations/versions/756de632f85b_user_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""user profile

Revision ID: 756de632f85b
Revises: 7e42683b12cd
Create Date: 2024-09-11 08:54:31.814650

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "756de632f85b"
down_revision = "7e42683b12cd"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"country_code",
sa.String(length=3),
server_default=sa.text("''"),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"bio",
sa.String(length=5000),
server_default=sa.text("''"),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"webpage",
sa.String(length=2048),
server_default=sa.text("''"),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"mastodon",
sa.String(length=500),
server_default=sa.text("''"),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"github",
sa.String(length=39),
server_default=sa.text("''"),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"linkedin",
sa.String(length=30),
server_default=sa.text("''"),
nullable=True,
)
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_column("linkedin")
batch_op.drop_column("github")
batch_op.drop_column("mastodon")
batch_op.drop_column("webpage")
batch_op.drop_column("bio")
batch_op.drop_column("country_code")
3 changes: 2 additions & 1 deletion website/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

from sqlalchemy import create_engine, text

from .user import User
from .bundle import Bundle
from .comment import Comment
from .user import User


__all__ = ["User", "Bundle", "Comment"]

Expand Down
80 changes: 73 additions & 7 deletions website/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import re
import secrets
from sqlalchemy import func
from urllib.parse import urlparse
from datetime import datetime

Expand All @@ -14,6 +15,8 @@
from validate_email import validate_email # type: ignore[import-untyped]
from werkzeug.security import check_password_hash

from website.models import Bundle
from website.models import Comment
from website.web.bootstrap import db
from website.web.bootstrap import application

Expand All @@ -30,15 +33,24 @@ class User(db.Model, UserMixin): # type: ignore[name-defined, misc]
id = db.Column(db.Integer, primary_key=True)
login = db.Column(db.String(30), unique=True, nullable=False)
name = db.Column(db.String(50), nullable=False)
organisation = db.Column(db.String(50), default="")
email = db.Column(db.String(256), nullable=False)
pwdhash = db.Column(db.String(), nullable=False)
secret_token = db.Column(db.String(), unique=True)
secret_token = db.Column(db.String(), unique=True) # for Two-Factor Authentication
is_two_factor_authentication_enabled = db.Column(
db.Boolean, nullable=False, default=False
)
email = db.Column(db.String(256), nullable=False)

organisation = db.Column(db.String(50), default="")
country_code = db.Column(db.String(3), default="") # for alpha-3 codes
bio = db.Column(db.String(5000), default="")
webpage = db.Column(db.String(2048), default="")
mastodon = db.Column(db.String(500), default="")
github = db.Column(db.String(39), default="")
linkedin = db.Column(db.String(30), default="")

created_at = db.Column(db.DateTime(), default=datetime.now)
last_seen = db.Column(db.DateTime(), default=datetime.now)

apikey = db.Column(db.String(100), default=generate_token, unique=True)

is_active = db.Column(db.Boolean(), default=True)
Expand Down Expand Up @@ -94,16 +106,53 @@ def validates_email(self, key: str, value: str) -> str:

@validates("name")
def validates_name(self, key: str, value: str) -> str:
assert 3 <= len(value) <= 256, AssertionError("Maximum length for name: 256")
assert 3 <= len(value) <= 50, AssertionError("Maximum length for name: 50")
value = value.strip()
return value

@validates("bio")
def validates_bio(self, key: str, value: str) -> str:
assert 0 <= len(value) <= 500, AssertionError("Maximum length for bio: 500")
value = value.strip()
# Remove external links
value = re.sub(r"\[.*?\]\(http[s]?://[^\)]+\)", "", value)
# Remove images
value = re.sub(r"!\[.*?\]\(.*?\)", "", value)
return value

@validates("country_code")
def validates_country_code(self, key: str, value: str) -> str:
assert 0 <= len(value) <= 3, AssertionError(
"Maximum length for country_code: 3"
)
value = value.strip()
return value

@validates("organisation")
def validates_organisation(self, key: str, value: str) -> str:
assert 3 <= len(value) <= 256, AssertionError(
"Maximum length for organisation: 256"
assert 0 <= len(value) <= 50, AssertionError(
"Maximum length for organisation: 50"
)
value = value.strip()
return re.sub("[^a-zA-Z0-9_-]", "", value.strip())

@validates("github")
def validates_github(self, key: str, value: str) -> str:
assert 0 <= len(value) <= 39, AssertionError("Maximum length for GitHub: 39")
if value.strip():
github_regex = r"^[a-zA-Z\d](?:[a-zA-Z\d]|-(?=[a-zA-Z\d])){0,38}$"
assert re.match(github_regex, value) is not None, AssertionError(
"Invalid GitHub username."
)
return value

@validates("linkedin")
def validates_linkedin(self, key: str, value: str) -> str:
assert 0 <= len(value) <= 30, AssertionError("Maximum length for LinkedIn: 30")
if value.strip():
linkedin_regex = r"^[a-zA-Z\d](?:[a-zA-Z\d-]{0,28}[a-zA-Z\d])?$"
assert re.match(linkedin_regex, value) is not None, AssertionError(
"Invalid LinkedIn username."
)
return value

@validates("apikey")
Expand All @@ -129,3 +178,20 @@ def is_otp_valid(self, user_otp: Any) -> bool:
"""Checks the validity of a One Time password."""
totp: TOTP = pyotp.parse_uri(self.get_authentication_setup_uri()) # type: ignore
return totp.verify(user_otp)

def nb_contributions(self) -> int:
"""Returns the number of contributions (creation of comments and bundles) of the user."""
user_contributions = (
db.session.query(
User.id,
(func.count(Comment.uuid) + func.count(Bundle.uuid)).label(
"total_contributions"
),
)
.outerjoin(Comment, User.id == Comment.author_id) # Join User with Comment
.outerjoin(Bundle, User.id == Bundle.author_id) # Join User with Bundle
.filter(User.id == self.id) # Filter by specific user ID
.group_by(User.id) # Group by User ID
.first()
)
return user_contributions[1]
1 change: 1 addition & 0 deletions website/web/api/v1/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def post(self) -> Tuple[ResultType, int]:
bundle.get("related_vulnerabilities", []).extend(
find_pysec_ids(bundle.get("description", ""))
)
bundle["related_vulnerabilities"] = list(set(bundle["related_vulnerabilities"]))

try:
validate_json(bundle, "circl_bundle")
Expand Down
4 changes: 3 additions & 1 deletion website/web/api/v1/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import logging
import uuid
from flask import jsonify
from flask_login import current_user # type: ignore[import-untyped]
from flask_restx import fields # type: ignore[import-untyped]
from flask_restx import abort
Expand Down Expand Up @@ -227,6 +226,9 @@ def post(self) -> Tuple[ResultType, int]:
comment.get("related_vulnerabilities", []).extend(
find_pysec_ids(comment.get("description", ""))
)
comment["related_vulnerabilities"] = list(
set(comment["related_vulnerabilities"])
)

# Validate the JSON payload
try:
Expand Down
Loading