Add markdown page, server-side autocomplete PoC, etc.

This commit is contained in:
Darryl Nixon 2023-05-28 19:49:54 -07:00
parent 9dab48a219
commit 8f6f566b71
15 changed files with 455 additions and 14 deletions

213
ghostforge/ethnicities.py Normal file
View file

@ -0,0 +1,213 @@
from typing import List
from fastapi import APIRouter
NATIONALITIES = (
"Afghan",
"Albanian",
"Algerian",
"American",
"Andorran",
"Angolan",
"Antiguans",
"Argentinean",
"Armenian",
"Australian",
"Austrian",
"Azerbaijani",
"Bahamian",
"Bahraini",
"Bangladeshi",
"Barbadian",
"Barbudans",
"Batswana",
"Belarusian",
"Belgian",
"Belizean",
"Beninese",
"Bhutanese",
"Bolivian",
"Bosnian",
"Brazilian",
"British",
"Bruneian",
"Bulgarian",
"Burkinabe",
"Burmese",
"Burundian",
"Cambodian",
"Cameroonian",
"Canadian",
"Cape Verdean",
"Central African",
"Chadian",
"Chilean",
"Chinese",
"Colombian",
"Comoran",
"Congolese",
"Costa Rican",
"Croatian",
"Cuban",
"Cypriot",
"Czech",
"Danish",
"Djibouti",
"Dominican",
"Dutch",
"Dutchman",
"Dutchwoman",
"East Timorese",
"Ecuadorean",
"Egyptian",
"Emirian",
"Equatorial Guinean",
"Eritrean",
"Estonian",
"Ethiopian",
"Fijian",
"Filipino",
"Finnish",
"French",
"Gabonese",
"Gambian",
"Georgian",
"German",
"Ghanaian",
"Greek",
"Grenadian",
"Guatemalan",
"Guinea-Bissauan",
"Guinean",
"Guyanese",
"Haitian",
"Herzegovinian",
"Honduran",
"Hungarian",
"I-Kiribati",
"Icelander",
"Indian",
"Indonesian",
"Iranian",
"Iraqi",
"Irish",
"Israeli",
"Italian",
"Ivorian",
"Jamaican",
"Japanese",
"Jordanian",
"Kazakhstani",
"Kenyan",
"Kittian and Nevisian",
"Kuwaiti",
"Kyrgyz",
"Laotian",
"Latvian",
"Lebanese",
"Liberian",
"Libyan",
"Liechtensteiner",
"Lithuanian",
"Luxembourger",
"Macedonian",
"Malagasy",
"Malawian",
"Malaysian",
"Maldivan",
"Malian",
"Maltese",
"Marshallese",
"Mauritanian",
"Mauritian",
"Mexican",
"Micronesian",
"Moldovan",
"Monacan",
"Mongolian",
"Moroccan",
"Mosotho",
"Motswana",
"Mozambican",
"Namibian",
"Nauruan",
"Nepalese",
"Netherlander",
"New Zealander",
"Ni-Vanuatu",
"Nicaraguan",
"Nigerian",
"Nigerien",
"North Korean",
"Northern Irish",
"Norwegian",
"Omani",
"Pakistani",
"Palauan",
"Panamanian",
"Papua New Guinean",
"Paraguayan",
"Peruvian",
"Polish",
"Portuguese",
"Qatari",
"Romanian",
"Russian",
"Rwandan",
"Saint Lucian",
"Salvadoran",
"Samoan",
"San Marinese",
"Sao Tomean",
"Saudi",
"Scottish",
"Senegalese",
"Serbian",
"Seychellois",
"Sierra Leonean",
"Singaporean",
"Slovakian",
"Slovenian",
"Solomon Islander",
"Somali",
"South African",
"South Korean",
"Spanish",
"Sri Lankan",
"Sudanese",
"Surinamer",
"Swazi",
"Swedish",
"Swiss",
"Syrian",
"Taiwanese",
"Tajik",
"Tanzanian",
"Thai",
"Togolese",
"Tongan",
"Trinidadian or Tobagonian",
"Tunisian",
"Turkish",
"Tuvaluan",
"Ugandan",
"Ukrainian",
"Uruguayan",
"Uzbekistani",
"Venezuelan",
"Vietnamese",
"Welsh",
"Yemenite",
"Zambian",
"Zimbabwean",
)
gf = APIRouter()
@gf.get("/ethnicities")
async def get_ethnicities(search: str) -> List[str]:
matching = [x for x in NATIONALITIES if search.lower() in x.lower()]
return matching

View file

@ -120,10 +120,15 @@ async def read_ghost(
.join(User, Ghost.owner_id == User.id) .join(User, Ghost.owner_id == User.id)
.where(Ghost.id == ghost_id) .where(Ghost.id == ghost_id)
) )
ghost = result.scalars().first() ghost = result.first()
if not ghost: if not ghost:
raise HTTPException(status_code=404, detail="No ghost with that ID") raise HTTPException(status_code=404, detail="No ghost with that ID")
data = {"ghost": ghost, "user": current_user, "crumbs": [("ghosts", "/ghosts"), (ghost.id, False)]} data = {
"ghost": ghost.Ghost,
"owner": ghost.owner_username,
"user": current_user,
"crumbs": [("ghosts", "/ghosts"), (ghost.Ghost.id, False)],
}
request.state.ghostforge = data | getattr(request.state, "ghostforge", {}) request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
return ghost return ghost

View file

@ -6,10 +6,11 @@ from typing import Callable
from fastapi import Request from fastapi import Request
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.engine.row import Row
from ghostforge.templates import templates from ghostforge.templates import templates
# Original credit: https://github.com/acmpo6ou/fastapi_html_json/blob/master/html_json.py # Adapted from original at https://github.com/acmpo6ou/fastapi_html_json/blob/master/html_json.py
class HtmlJson: class HtmlJson:
@ -50,6 +51,11 @@ class HtmlJson:
result = result.dict() result = result.dict()
elif isinstance(result, list): elif isinstance(result, list):
result = {"data": result[:]} result = {"data": result[:]}
elif isinstance(result, Row):
tmp = {}
for k in result.keys():
tmp[k] = result[k]
result = tmp
if hasattr(request.state, "ghostforge"): if hasattr(request.state, "ghostforge"):
for key, value in request.state.ghostforge.items(): for key, value in request.state.ghostforge.items():

View file

@ -0,0 +1,10 @@
# Security Decisions
Setting up the application on a self-hosted server for maximum privacy
# Updating
Best practices for keeping the application and your data secure and up to date.
# Troubleshooting
Troubleshooting common issues and errors in the application

View file

@ -0,0 +1,51 @@
# Guidebook
## Introduction
In today's digital age, privacy is more important than ever. With multinational corporations collecting massive amounts of data on their customers (and even non-customers), many people are necessarily turning to privacy-centric tools to protect their digital identities. <kbd>ghostforge</kbd> is a full-featured false persona management application. Essentially, it's a self-hosted open source platform designed to help users manage, plan, and or track their activity while masquerading as someone other than themselves a ghost.
With ghostforge, users can be used for a variety of purposes, from maintaining privacy while browsing the internet to more complex operations like digital activism, red teaming, investigative journalism, and more. The platform provides a secure and customizable way to manage sensitive information related to each ghost, such as passwords, credit cards, and contact information, all while encouraging strict operational security to prevent compromise. In this guidebook, we will explore the various features of ghostforge, discuss best practices for maintaining operational security and privacy, and examine potential legal and ethical considerations surrounding the use of ghosts.
## Privacy-First Internet Use
In today's world of hyper-connectedness, online privacy has become increasingly important. With websites and ad tech companies constantly tracking and collecting user data, individuals are at risk of having their personal information compromised or sold to third-party advertisers without their consent. Additionally, cybercrime and organized criminals often take advantage of online security weaknesses to steal personal information and perpetrate fraud.
One way to combat this is through privacy-first internet use, which involves using tools and techniques to protect personal data and online identity. Anonymous browsing is a key component of this, allowing users to browse the internet without revealing identifying information such as IP address or location. Tools like the Tor Browser and VPNs can be used to achieve anonymous browsing, providing increased security for online browsing and helping individuals avoid unwanted tracking and profiling by ad tech companies. On top of this, while signing up for internet-based services, associating accounts with a ghost instead of a true identity can help hide your collective internet activity from interested parties. ghostforge provides a safe place to store credentials and payment information for such services, enabling a true point of demarcation between your true identity and each individual ghost.
Secure communication is also an important element of privacy-first internet use. Encrypted communication tools like Signal, Telegram, and ProtonMail provide secure channels for messaging and email correspondence, protecting users from prying eyes and safeguarding sensitive information. These tools help to ensure that communications remain private and cannot be accessed by third parties, providing individuals with much-needed peace of mind. Likewise, ghostforge does not provide a secure messaging apparatus, but provides a secure storage facility for credentials for such services.
## How to be a Good Ghost
Creating and managing ghosts (or, "ghosting") is a delicate balance between believability and operational security. While it is important to create a believable persona, it is equally important to ensure that the data associated with the ghost cannot be traced back to the true user.
By following these best practices, users can create realistic ghost profiles that can be used for various purposes without risking exposure. However, it's important to note that proper operational security measures should always be taken to prevent leaks or mistakes that could compromise the authenticity of the persona.
#### Start with a basic persona
When creating a ghost, it's important to start with a basic persona that reflects the characteristics of individuals within a specific demographic or age group. It's important to tailor the persona to the intended purpose and select attributes that are common for that age group.
#### Research thoroughly and be patient
Research is essential in creating an authentic and believable ghost profile. As a rule of thumb, avoid using information from personal experiences and instead research the characteristics, preferences, and interests of individuals within the ghost demographic. Social media websites like Facebook, Twitter, and LinkedIn can help in gathering data while avoiding suspicions by creating numerous accounts on the social media sites.
Social media sites and many other web services rightly have false identity detection mitigations in place to hamper the ability for accounts to be created arbitrarily. In some cases, these mitigations can be based on risk analysis of initial user activity, but are more often supplemented by the use of third party ad technology services. It can be difficult to sufficiently build an advertisement identity for a ghost in a short period of time, so be patient and consider developing multiple ghosts at once in parallel but in segregated environments (e.g., different VPNs, VMs)
#### Maintain consistency
Ensure consistency in all aspects of the ghost profile, including name, age, geographic location, occupation, interests, and education. The persona should be carefully crafted to make it difficult for anyone to realize that it is fake. Supplementary information should be appropriate for that demographic a 21-year-old bartender is less likely to have a Ph.D. and international assets, whereas it may be more believable for them to have hundreds of dollars in cryptocurrencies and a bevy of inactive social media accounts.
#### Use appropriate identifiers, services, and configurations
A key aspect of creating a believable ghost profile is to select usernames and service providers that are appropriate to the persona. For example, names and email providers that align with the demographic, culture, or language will give the ghost more credibility and reduce the likelihood of detection. Likewise, international ghosts that primarily use a foreign language might be predisposed to a more popular search engine in their locality and would be less likely to immediately set their language to English.
#### Financial information
When it's legal to prepare and operate financial assets and liabilities like credit cards with a ghost, ensure that the details match the ghost profile's overall financial situation while still maintaining strong operational security. It's important to consider which types of financial services are common for the demographic and tailor the profile accordingly.
## Legality of Ghosts
The use of false personas (ghosts) raises important legal and ethical questions around when, where, and how they may be used. Use of web services often necessitate agreement with end-user license agreements (EULAs), which govern how those users may access and use the service. These agreements often contain clauses prohibiting the use of false identities or personas, and violating these terms can result in the suspension or termination of the user's account. However, the legality of using false identities on web services is a complex issue, with laws differing across jurisdictions.
In major countries like the United States, using false personas for web services is generally not considered illegal. However, when it comes to banking and financial transactions, using false identities can be viewed as fraudulent and can easily result in prosecution under local or federal law. It is important that individuals using false identities for financial purposes understand the potential legal risks they may be taking on and take necessary steps to protect themselves.
It is important for users of ghostforge to understand that they are responsible for following the laws and regulations of their jurisdiction when using the software. The developers of ghostforge are not responsible for any activity, legal or not, carried out using the platform and cannot be held liable for any consequences resulting from deliberate misuse of the software. It remains the responsibility of the users to use software responsibly and ensure their own compliance with their own relevant laws and regulations.
## Managing your Credentials
Managing passwords and other sensitive information is an essential part of maintaining security and remains essential when creating and managing ghosts. While ghostforge is not intended to function as a dynamic password manager, users should still use sane and modern methods to securely generate and manage passwords. One option is to use a trusted password manager, such as Bitwarden, KeePass, or 1Password, in conjunction with ghostforge. This approach can help ensure password security while keeping consistent historical tracking for each ghost for any and all password changes.
The ghostforge interface provides functonality to securely generate and manage passwords using the current NIST guidelines. NIST recommends using lengthy rather than overly complex passwords, as longer phrases are harder to crack through brute force attacks. By choosing long, unique phrases for each ghost profile, users can significantly reduce the likelihood of their passwords being guessed or brute-forced.
One of the advantages of using ghostforge for password management is that it maintains the complete history of password changes for each ghost profile, which can be useful when identifying which password was in use at a snapshot in time. It is important to keep the passwords of each ghost up-to-date and change them in cases of compromise to maintain security.
In summary, while ghostforge may not currently function as a dynamic password manager, it offers robust password management features and may eventually be integrated with trusted password managers. It is our recommendation that users follow the current best practice guidelines for password decisions to ensure the highest level of security for their ghost profiles.

33
ghostforge/pages.py Normal file
View file

@ -0,0 +1,33 @@
from pathlib import Path
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi.responses import HTMLResponse
from ghostforge.templates import templates
from ghostforge.users import get_current_user
from markdown import markdown
gf = APIRouter()
markdown_dir = Path("ghostforge/markdown/")
def render_markdown(md_file):
with open(markdown_dir / md_file, "r") as f:
return markdown(f.read())
@gf.get("/guidebook", response_class=HTMLResponse)
async def guidebook_page(request: Request, current_user=Depends(get_current_user(optional=True))):
if not current_user:
return HTTPException(status_code=401, detail="Not authenticated")
data = {
"title": "Guidebook",
"html": render_markdown("guidebook.md"),
"user": current_user,
"crumbs": [("research", False), ("guidebook", False)],
}
return templates.TemplateResponse("pages.html", data | {"request": request})

View file

@ -10,7 +10,9 @@ from fastapi.staticfiles import StaticFiles
from ghostforge.db import create_db_and_tables from ghostforge.db import create_db_and_tables
from ghostforge.db import User from ghostforge.db import User
from ghostforge.ethnicities import gf as gf_ethnicities
from ghostforge.ghosts import gf as gf_ghosts from ghostforge.ghosts import gf as gf_ghosts
from ghostforge.pages import gf as gf_pages
from ghostforge.templates import templates from ghostforge.templates import templates
from ghostforge.users import fastapi_users from ghostforge.users import fastapi_users
from ghostforge.users import get_current_user from ghostforge.users import get_current_user
@ -24,6 +26,8 @@ from ghostforge.users import web_backend
gf = FastAPI() gf = FastAPI()
gf.mount("/static", StaticFiles(directory="ghostforge/static"), name="static") gf.mount("/static", StaticFiles(directory="ghostforge/static"), name="static")
gf.include_router(gf_ghosts) gf.include_router(gf_ghosts)
gf.include_router(gf_pages)
gf.include_router(gf_ethnicities)
gf.include_router(fastapi_users.get_auth_router(jwt_backend), prefix="/auth/jwt", tags=["auth"]) gf.include_router(fastapi_users.get_auth_router(jwt_backend), prefix="/auth/jwt", tags=["auth"])
gf.include_router(fastapi_users.get_auth_router(web_backend), prefix="/auth/cookie", tags=["auth"]) gf.include_router(fastapi_users.get_auth_router(web_backend), prefix="/auth/cookie", tags=["auth"])

View file

@ -6,7 +6,7 @@ function switchTab(tab) {
x[i].style.display = "none"; x[i].style.display = "none";
} }
for (i = 0; i < a.length; i++) { for (i = 0; i < a.length; i++) {
if (a[i].id.endsWith(tab)) { if (a[i].id.startsWith("Tab") && a[i].id.endsWith(tab)) {
a[i].classList.add("active"); a[i].classList.add("active");
} else { } else {
a[i].classList.remove("active"); a[i].classList.remove("active");

View file

@ -27,7 +27,6 @@ templates.env.globals["gf_navbar"] = {
"Manage Users": "/manage", "Manage Users": "/manage",
}, },
"Meta": { "Meta": {
"About GhostForge": "/about",
"System Logs": "/logs", "System Logs": "/logs",
"Logout": "/logout", "Logout": "/logout",
}, },

View file

@ -3,12 +3,13 @@
{% block content %} {% block content %}
<div class="columns"> <div class="columns">
<div class="column col-12"> <div class="column col-12">
<h1><span class="text-light bg-dark ghost_view_title">{% block title %} <h1><span class="text-light bg-dark ghost_view_title">
{{ {% block title %}
ghost.first_name }} {{ {{ ghost.first_name }}
ghost.middle_name {{ ghost.middle_name }}
}} {{ {{ ghost.last_name }}
ghost.last_name }}{% endblock title %}</span> {% endblock title %}
</span>
</h1> </h1>
<h5>[age] year old American Male</h5> <h5>[age] year old American Male</h5>

View file

@ -1,5 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Browse Ghosts{% endblock title %}
{% block js %} {% block js %}
<script src="{{ url_for('static', path='/js/jquery-3.7.0.min.js') }}"></script> <script src="{{ url_for('static', path='/js/jquery-3.7.0.min.js') }}"></script>
<script src="{{ url_for('static', path='/js/colReorder.dataTables.min.js') }}"></script> <script src="{{ url_for('static', path='/js/colReorder.dataTables.min.js') }}"></script>

View file

@ -11,7 +11,7 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Ethnicities <!-- <label class="form-label">Ethnicities
<div class="form-group"> <div class="form-group">
<select class="form-select form-inline" multiple> <select class="form-select form-inline" multiple>
<option>German</option> <option>German</option>
@ -23,5 +23,114 @@
<option>Chinese</option> <option>Chinese</option>
</select> </select>
</div> </div>
</label> -->
<label class="form-label">Ethnicities
<div class="form-autocomplete">
<div class="form-autocomplete-input form-input" id="ethnicity_chips">
<input class="form-input" type="text" id="ethnicities">
</div>
<ul class="menu" id="ethnicities_menu" style="display: none;">
<li class="menu-item">
<a href="#">
<div class="tile tile-centered">
</div>
</a>
</li>
</ul>
</div>
</label> </label>
</div> </div>
<script>
// Get the input element
const input = document.getElementById("ethnicities");
// Get the menu element
const menu = document.getElementById('ethnicities_menu');
const menuItems = menu.querySelectorAll("#ethnicities_menu li.menu-item");
// Create an event listener for input changes
input.addEventListener("input", async () => {
// Get the current input value
const value = input.value;
// Clear the existing menu items
menu.innerHTML = "";
// Reset the menu items with original list
menu.append(...menuItems);
// If the input is blank, do not show the autocomplete list
if (!value.trim()) {
menu.style.display = "none";
return;
}
// Send an AJAX request to the backend
const response = await fetch(`/ethnicities?search=${value}`);
const ethnicities = await response.json();
// Add a list item for each ethnicity that matches the search
for (const ethnicity of ethnicities) {
const tile = document.createElement("div");
tile.classList.add("tile", "tile-centered");
tile.innerHTML = `
<div class="tile-content">${ethnicity}</div>
`;
const link = document.createElement("a");
link.setAttribute("href", "#");
link.appendChild(tile);
const listItem = document.createElement("li");
listItem.classList.add("menu-item");
listItem.appendChild(link);
menu.appendChild(listItem);
}
menu.style.display = "block";
});
// Create an event listener for clicks on the menu items
menu.addEventListener("click", (event) => {
// Check if the target is a menu item
if (event.target.closest(".menu-item")) {
// Get the selected ethnicity
const selected = event.target.closest(".menu-item").querySelector(".tile-content").textContent;
// Create a chip element
const chip = document.createElement("div");
chip.classList.add("chip");
chip.textContent = selected;
// Add a close button
const closeButton = document.createElement("a");
closeButton.classList.add("btn", "btn-clear");
closeButton.setAttribute("aria-label", "Close");
closeButton.setAttribute("role", "button");
closeButton.addEventListener("click", () => {
chip.remove();
});
chip.appendChild(closeButton);
// Insert the chip before the input element
input.before(chip);
// Clear the input text
input.value = "";
// Close the autocomplete menu
menu.style.display = "none";
}
});
// Create an event listener for clicks on the clear buttons
const clearButtons = document.querySelectorAll("#ethnicity_chips .btn-clear");
for (const clearButton of clearButtons) {
clearButton.addEventListener("click", (event) => {
event.preventDefault();
event.target.closest(".chip").remove();
});
}
</script>

View file

@ -9,7 +9,7 @@
<a href="#" onclick="switchTab('Acquisitions')" class="profile_tab_link" id="TabAcquisitions">Acquisitions</a> <a href="#" onclick="switchTab('Acquisitions')" class="profile_tab_link" id="TabAcquisitions">Acquisitions</a>
</li> </li>
<li class="tab-item"> <li class="tab-item">
<a href="#" onclick="switchTab('Meta')" class="profile_tab_link" id="TabMeta">Meta</a> <a href="#" onclick="switchTab('History')" class="profile_tab_link" id="TabHistory">History</a>
</li> </li>
</ul> </ul>
<form> <form>
@ -34,7 +34,7 @@
<div id="History" class="profile_tab" style="display:none"> <div id="History" class="profile_tab" style="display:none">
<div class="card-header"> <div class="card-header">
<div class="card-title h5">Meta</div> <div class="card-title h5">History</div>
<div class="card-subtitle text-gray">"now where was i..."</div> <div class="card-subtitle text-gray">"now where was i..."</div>
</div> </div>
<div class="card-body">History is very important</div> <div class="card-body">History is very important</div>

View file

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock title %}
{% block content %}
{{ html | safe }}
{% endblock content %}

View file

@ -23,6 +23,7 @@ dependencies = [
"fastapi-users==11.0.0", "fastapi-users==11.0.0",
"fastapi-users-db-sqlmodel==0.3.0", "fastapi-users-db-sqlmodel==0.3.0",
"sqlmodel==0.0.8", "sqlmodel==0.0.8",
"markdown==3.4.3",
] ]
[project.urls] [project.urls]